1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
|
from pathlib import Path
from fpdf import FPDF
from fpdf.enums import XPos, YPos
from test.conftest import assert_pdf_equal
import pytest
HERE = Path(__file__).resolve().parent
def test_named_destinations(tmp_path):
"""Test PDF generation with named destinations and links.
This test demonstrates how to create a hierarchical structure of named destinations
by using named destinations directly and binding them to pages later.
"""
# Create PDF
pdf = FPDF()
# Create the first page first
pdf.add_page()
# Create the main TOC destination directly
pdf.set_link(name="dests/main")
# For backward compatibility, you can still use add_link with named destinations
# We only need to create a link for subsection1 this way for testing
sub1_link = pdf.add_link(name="dests/subsection1", page=2)
# Subsections 2 and 3 will be referenced directly by name
# without pre-creating them
# First page is already created
pdf.set_font("Helvetica", size=16)
# Main TOC destination is already bound to this location
pdf.cell(0, 10, "Table of Contents", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
pdf.ln(10)
pdf.set_font("Helvetica", size=12)
# Add links to subsections in the TOC
pdf.cell(
0,
10,
"Subsection 1: Concepts",
link=sub1_link,
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# You can directly reference a destination by name
# Even if it hasn't been set yet
pdf.cell(
0,
10,
"Subsection 2: Implementation",
link="#dests/subsection2", # Using the '#' prefix to reference the named destination
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# Use direct named reference for subsection 3 too
pdf.cell(
0,
10,
"Subsection 3: Advanced Usage",
link="#dests/subsection3", # Named destinations automatically created when referenced
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# Subsection 1 page
pdf.add_page()
pdf.set_font("Helvetica", size=16)
# Bind subsection1 destination to this location
pdf.set_link(sub1_link)
pdf.cell(0, 10, "Subsection 1: Concepts", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
pdf.set_font("Helvetica", size=12)
pdf.multi_cell(
0,
10,
(
"This page demonstrates binding a previously created named destination. "
"The destination 'dests/subsection1' was created before this page existed "
"and is now bound to this location.\n\n"
"Click on the table of contents link to return."
),
)
# Add a link back to TOC
pdf.cell(
0,
10,
"Return to Table of Contents",
link="#dests/main",
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# Subsection 2 page
pdf.add_page()
pdf.set_font("Helvetica", size=16)
# Bind subsection2 destination to this location using name directly
pdf.set_link(name="dests/subsection2")
pdf.cell(0, 10, "Subsection 2: Implementation", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
pdf.set_font("Helvetica", size=12)
pdf.multi_cell(
0,
10,
(
"This page shows how named destinations maintain stability when page numbers "
"change. Even if pages are added or removed, the named destination 'dests/subsection2' "
"will always point to this content.\n\n"
"Click on the table of contents link to return."
),
)
# Add a link back to TOC
pdf.cell(
0,
10,
"Return to Table of Contents",
link="#dests/main",
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# Subsection 3 page
pdf.add_page()
pdf.set_font("Helvetica", size=16)
# Bind subsection3 destination to this location using name directly
pdf.set_link(name="dests/subsection3")
pdf.cell(0, 10, "Subsection 3: Advanced Usage", new_x=XPos.LMARGIN, new_y=YPos.NEXT)
pdf.set_font("Helvetica", size=12)
pdf.multi_cell(
0,
10,
(
"Named destinations can be referenced anywhere in the document, including in "
"bookmarks, outlines, and from external documents. The hierarchical naming scheme "
"'dests/subsection3' allows for organized structure.\n\n"
"Click on the table of contents link to return."
),
)
# Add a link back to TOC using direct named reference
pdf.cell(
0,
10,
"Return to Table of Contents",
link="#dests/main",
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
)
# Compare generated PDF to reference using assert_pdf_equal
assert_pdf_equal(pdf, HERE / "named_destinations.pdf", tmp_path)
def test_invalid_destination():
"""Test that using non-existent index still raises a KeyError."""
pdf = FPDF()
pdf.add_page()
with pytest.raises(KeyError):
pdf.set_link(99999)
def test_duplicate_destinations():
"""Test that creating multiple destinations with the same name works."""
pdf = FPDF()
pdf.add_page()
link1 = pdf.add_link(name="test")
link2 = pdf.add_link(name="test")
# Should not raise any error when setting either link
pdf.set_link(link1)
pdf.set_link(link2)
def test_set_link_with_name():
"""Test setting a named destination using set_link with name parameter."""
pdf = FPDF()
pdf.add_page()
# Create a named destination with set_link
name = pdf.set_link(name="direct_destination")
# Verify the name was returned
assert name == "direct_destination"
# Verify the destination exists
assert "direct_destination" in pdf.named_destinations
# Create another page and reference the destination
pdf.add_page()
pdf.set_font("Helvetica", size=12)
pdf.cell(0, 10, "Link to first page", link="#direct_destination")
# This should not raise an error
dest_link = pdf.get_named_destination("direct_destination")
assert dest_link is not None
def test_unset_destinations_error():
"""Test that referencing but not setting a destination raises an error."""
pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", size=12)
# Reference a destination but don't set it
pdf.cell(0, 10, "Link to nowhere", link="#missing-destination")
# Trying to output should raise an exception
with pytest.raises(Exception) as excinfo:
pdf.output(HERE / "should_not_be_created.pdf")
# Verify error message contains the missing destination name
assert "missing-destination" in str(excinfo.value)
|