28
28
from enum import IntEnum
29
29
from functools import wraps
30
30
from pathlib import Path
31
+ from typing import NamedTuple , Optional
31
32
32
33
from .errors import FPDFException , FPDFPageFormatException
33
34
from .fonts import fpdf_charwidths
@@ -83,6 +84,15 @@ class DocumentState(IntEnum):
83
84
CLOSED = 3 # EOF printed
84
85
85
86
87
+ class PageLink (NamedTuple ):
88
+ x : int
89
+ y : int
90
+ width : int
91
+ height : int
92
+ link : str
93
+ alt_text : Optional [str ] = None
94
+
95
+
86
96
# Disabling this check due to the "format" parameter below:
87
97
# pylint: disable=redefined-builtin
88
98
def get_page_format (format , k = None ):
@@ -172,7 +182,7 @@ def __init__(
172
182
self .font_files = {} # array of font files
173
183
self .diffs = {} # array of encoding differences
174
184
self .images = {} # array of used images
175
- self .page_links = {} # array of links in pages
185
+ self .page_links = {} # array of PageLink
176
186
self .links = {} # array of internal links
177
187
self .in_footer = 0 # flag set when processing footer
178
188
self .lasth = 0 # height of last cell printed
@@ -191,7 +201,7 @@ def __init__(
191
201
self .angle = 0 # used by deprecated method: rotate()
192
202
self .font_cache_dir = font_cache_dir
193
203
self ._marked_contents = [] # list of MarkedContent
194
- self ._struct_parents_id_per_page = {} # {page_object_id -> StructParents ID}
204
+ self ._struct_parents_id_per_page = {} # {page_object_id -> StructParent(s) ID}
195
205
# Only set if a Structure Tree is added to the document:
196
206
self ._struct_tree_root_obj_id = None
197
207
@@ -927,13 +937,33 @@ def set_link(self, link, y=0, page=-1):
927
937
928
938
self .links [link ] = [page , y ]
929
939
930
- def link (self , x , y , w , h , link , alt_text = "" ):
931
- """Put a link on the page"""
940
+ def link (self , x , y , w , h , link , alt_text = None ):
941
+ """
942
+ Puts a link on a rectangular area of the page.
943
+ Text or image links are generally put via [cell](#fpdf.FPDF.cell),
944
+ [write](#fpdf.FPDF.write) or [image](#fpdf.FPDF.image),
945
+ but this method can be useful for instance to define a clickable area inside an image.
946
+
947
+ Args:
948
+ x (int): horizontal position (from the left) to the left side of the link rectangle
949
+ y (int): vertical position (from the top) to the bottom side of the link rectangle
950
+ w (int): width of the link rectangle
951
+ h (int): width of the link rectangle
952
+ link (str): either an URL or a integer returned by `add_link`, defining an internal link to a page
953
+ alt_text (str): optional textual description of the link, for accessibility purposes
954
+ """
932
955
if self .page not in self .page_links :
933
956
self .page_links [self .page ] = []
934
- self .page_links [self .page ] += [
935
- (x * self .k , self .h_pt - y * self .k , w * self .k , h * self .k , link , alt_text )
936
- ]
957
+ self .page_links [self .page ].append (
958
+ PageLink (
959
+ x * self .k ,
960
+ self .h_pt - y * self .k ,
961
+ w * self .k ,
962
+ h * self .k ,
963
+ link ,
964
+ alt_text ,
965
+ )
966
+ )
937
967
938
968
@check_page
939
969
def text (self , x , y , txt = "" ):
@@ -1559,9 +1589,9 @@ def image(
1559
1589
type (str): [**DEPRECATED**] unused, will be removed in a later version.
1560
1590
link (str): optional link to add on the image, internal
1561
1591
(identifier returned by `add_link`) or external URL.
1562
- title (str): optional
1592
+ title (str): optional. Currently, never seem rendered by PDF readers.
1563
1593
alt_text (str): optional alternative text describing the image,
1564
- for accessibility purposes
1594
+ for accessibility purposes. Displayed by some PDF readers on hover.
1565
1595
"""
1566
1596
if type :
1567
1597
warnings .warn (
@@ -1615,20 +1645,25 @@ def image(
1615
1645
@contextmanager
1616
1646
def _marked_sequence (self , ** kwargs ):
1617
1647
page_object_id = self ._current_page_object_id ()
1618
- struct_parents_id = self ._struct_parents_id_per_page .get (page_object_id )
1619
- if struct_parents_id is None :
1620
- struct_parents_id = len (self ._struct_parents_id_per_page )
1621
- self ._struct_parents_id_per_page [page_object_id ] = struct_parents_id
1622
1648
mcid = sum (
1623
1649
1 for mc in self ._marked_contents if mc .page_object_id == page_object_id
1624
1650
)
1625
- self ._marked_contents . append (
1626
- MarkedContent ( page_object_id , struct_parents_id , mcid , ** kwargs )
1651
+ self ._add_marked_content (
1652
+ page_object_id , struct_type = "/Figure" , mcid = mcid , ** kwargs
1627
1653
)
1628
1654
self ._out (f"/P <</MCID { mcid } >> BDC" )
1629
1655
yield
1630
1656
self ._out ("EMC" )
1631
1657
1658
+ def _add_marked_content (self , page_object_id , ** kwargs ):
1659
+ struct_parents_id = self ._struct_parents_id_per_page .get (page_object_id )
1660
+ if struct_parents_id is None :
1661
+ struct_parents_id = len (self ._struct_parents_id_per_page )
1662
+ self ._struct_parents_id_per_page [page_object_id ] = struct_parents_id
1663
+ marked_content = MarkedContent (page_object_id , struct_parents_id , ** kwargs )
1664
+ self ._marked_contents .append (marked_content )
1665
+ return marked_content
1666
+
1632
1667
def _current_page_object_id (self ):
1633
1668
# Predictable given that _putpages is invoked first in _enddoc:
1634
1669
return 2 * self .page + 1
@@ -1758,8 +1793,8 @@ def _putpages(self):
1758
1793
for pl in self .page_links [n ]:
1759
1794
# first four things in 'link' list are coordinates?
1760
1795
rect = (
1761
- f"{ pl [ 0 ] :.2f} { pl [ 1 ] :.2f} "
1762
- f"{ pl [ 0 ] + pl [ 2 ] :.2f} { pl [ 1 ] - pl [ 3 ] :.2f} "
1796
+ f"{ pl . x :.2f} { pl . y :.2f} "
1797
+ f"{ pl . x + pl . width :.2f} { pl . y - pl . height :.2f} "
1763
1798
)
1764
1799
1765
1800
# start the annotation entry
@@ -1772,24 +1807,30 @@ def _putpages(self):
1772
1807
f"/F 4"
1773
1808
)
1774
1809
1775
- # HTML ending of annotation entry
1776
- if isinstance (pl [4 ], str ):
1777
- annots += f"/A <</S /URI /URI { enclose_in_parens (pl [4 ])} >>>>"
1810
+ if pl .alt_text is not None :
1811
+ # Note: the spec indicates that a /StructParent could be added **inside* this /Annot,
1812
+ # but tests with Adobe Acrobat Reader reveal that the page /StructParents inserted below
1813
+ # is enough to link the marked content in the hierarchy tree with this annotation link.
1814
+ self ._add_marked_content (
1815
+ self .n , struct_type = "/Link" , alt_text = pl .alt_text
1816
+ )
1778
1817
1779
- # Dest type ending of annotation entry
1780
- else :
1781
- assert pl [4 ] in self .links , (
1818
+ # HTML ending of annotation entry
1819
+ if isinstance (pl .link , str ):
1820
+ annots += f"/A <</S /URI /URI { enclose_in_parens (pl .link )} >>"
1821
+ else : # Dest type ending of annotation entry
1822
+ assert pl .link in self .links , (
1782
1823
f"Page { n } has a link with an invalid index: "
1783
- f"{ pl [ 4 ] } (doc #links={ len (self .links )} )"
1824
+ f"{ pl . link } (doc #links={ len (self .links )} )"
1784
1825
)
1785
- l = self .links [pl [ 4 ] ]
1786
- # if l [0] in self.orientation_changes: h = w_pt
1787
- # else: h = h_pt
1826
+ link = self .links [pl . link ]
1827
+ # if link [0] in self.orientation_changes: h = w_pt
1828
+ # else: h = h_pt
1788
1829
annots += (
1789
- f"/Dest [{ 1 + 2 * l [0 ]} 0 R /XYZ 0 "
1790
- f"{ h_pt - l [1 ] * self .k :.2f} null]>> "
1830
+ f"/Dest [{ 1 + 2 * link [0 ]} 0 R /XYZ 0 "
1831
+ f"{ h_pt - link [1 ] * self .k :.2f} null]"
1791
1832
)
1792
-
1833
+ annots += ">>"
1793
1834
# End links list
1794
1835
self ._out (f"{ annots } ]" )
1795
1836
if self .pdf_version > "1.3" :
0 commit comments