diff --git a/docs/Links.md b/docs/Links.md index cb2b4dcf4..ec1af546c 100644 --- a/docs/Links.md +++ b/docs/Links.md @@ -14,11 +14,33 @@ from fpdf import FPDF pdf = FPDF() pdf.add_page() pdf.set_font("helvetica", size=24) -pdf.cell(w=40, h=10, txt="Cell link", border=1, align="C", link="https://github.com/PyFPDF/fpdf2") +pdf.cell(txt="Cell link", border=1, center=True, + link="https://github.com/PyFPDF/fpdf2") pdf.output("hyperlink.pdf") ``` +## Hyperlink with FPDF.multi_cell ## + +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.set_font("helvetica", size=24) +pdf.add_page() +pdf.multi_cell( + pdf.epw, + txt="**Website:** [fpdf2](https://pyfpdf.github.io/fpdf2/) __Go visit it!__", + markdown=True, +) +pdf.output("hyperlink.pdf") +``` + +Links defined this way in Markdown can be styled by setting `FPDF` class attributes `MARKDOWN_LINK_COLOR` (default: `None`) & `MARKDOWN_LINK_UNDERLINE` (default: `True`). + +`link="https://...your-url"` can also be used to make the whole cell clickable. + + ## Hyperlink with FPDF.link ## The `FPDF.link` is a low-level method that defines a rectangular clickable area. @@ -59,6 +81,8 @@ The hyperlinks defined this way will be rendered in blue with underline. ## Internal links ## +Internal links are links redirecting to other pages in the document. + Using `FPDF.cell`: ```python @@ -67,16 +91,44 @@ from fpdf import FPDF pdf = FPDF() pdf.set_font("helvetica", size=24) pdf.add_page() -# Displaying a full-width cell with centered text: -pdf.cell(w=pdf.epw, txt="Welcome on first page!", align="C") +pdf.cell(txt="Welcome on first page!", align="C", center=True) pdf.add_page() link = pdf.add_link(page=1) pdf.cell(txt="Internal link to first page", border=1, link=link) pdf.output("internal_link.pdf") ``` -Similarly, `FPDF.link` can be used instead of `FPDF.cell`, -however `write_html` does not allow to define internal links. +Other methods can also insert internal links: + +* [FPDF.multi_cell](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) using `link=` **or** `markdown=True` and this syntax: `[link text](page number)` +* [FPDF.link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.link) +* [FPDF.write_html](HTML.md) using anchor tags: `link text` + +The unit tests `test_internal_links()` in [test_links.py](https://github.com/PyFPDF/fpdf2/blob/master/test/test_links.py) provides examples for all of those methods. + + +## Links to other documents on the filesystem ## + +Using `FPDF.cell`: + +```python +from fpdf import FPDF + +pdf = FPDF() +pdf.set_font("helvetica", size=24) +pdf.add_page() +pdf.cell(txt="Link to other_doc.pdf", border=1, link="other_doc.pdf") +pdf.output("link_to_other_doc.pdf") +``` + +Other methods can also insert internal links: + +* [FPDF.multi_cell](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) using `link=` **or** `markdown=True` and this syntax: `[link text](other_doc.pdf)` +* [FPDF.link](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.link) +* [FPDF.write_html](HTML.md) using anchor tags: `link text` + +The unit test `test_link_to_other_document()` in [test_links.py](https://github.com/PyFPDF/fpdf2/blob/master/test/test_links.py) provides examples for all of those methods. + ## Alternative description ## diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 5e00aa311..5a15d0820 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -229,6 +229,7 @@ class FPDF(GraphicsStateMixin): MARKDOWN_UNDERLINE_MARKER = "--" MARKDOWN_LINK_REGEX = re.compile(r"^\[([^][]+)\]\(([^()]+)\)(.*)$") MARKDOWN_LINK_COLOR = None + MARKDOWN_LINK_UNDERLINE = True HTML2FPDF_CLASS = HTML2FPDF @@ -2934,13 +2935,13 @@ def _render_styled_text_line( underlines.append( (self.x + dx + s_width, frag_width, frag.font, frag.font_size) ) - if frag.url: + if frag.link: self.link( x=self.x + dx + s_width, y=self.y + (0.5 * h) - (0.5 * frag.font_size), w=frag_width, h=frag.font_size, - link=frag.url, + link=frag.link, ) s_width += frag_width @@ -3166,14 +3167,19 @@ def frag(): continue is_link = self.MARKDOWN_LINK_REGEX.match(txt) if is_link: - link_text, link_url, txt = is_link.groups() + link_text, link_dest, txt = is_link.groups() if txt_frag: yield frag() gstate = self._get_current_graphics_state() - gstate["underline"] = True + gstate["underline"] = self.MARKDOWN_LINK_UNDERLINE if self.MARKDOWN_LINK_COLOR: gstate["text_color"] = self.MARKDOWN_LINK_COLOR - yield Fragment(list(link_text), gstate, self.k, url=link_url) + try: + page = int(link_dest) + link_dest = self.add_link(page=page) + except ValueError: + pass + yield Fragment(list(link_text), gstate, self.k, link=link_dest) continue if self.is_ttf_font and txt[0] != "\n" and not ord(txt[0]) in font_glyphs: style = ("B" if in_bold else "") + ("I" if in_italics else "") diff --git a/fpdf/line_break.py b/fpdf/line_break.py index 3d8a335af..56c71a96d 100644 --- a/fpdf/line_break.py +++ b/fpdf/line_break.py @@ -6,7 +6,7 @@ They may change at any time without prior warning or any deprecation period. """ -from typing import NamedTuple, Any, Union, Sequence +from typing import NamedTuple, Any, Optional, Union, Sequence from .enums import CharVPos, WrapMode from .errors import FPDFException @@ -27,7 +27,7 @@ def __init__( characters: Union[list, str], graphics_state: dict, k: float, - url: str = None, + link: Optional[Union[int, str]] = None, ): if isinstance(characters, str): self.characters = list(characters) @@ -35,7 +35,7 @@ def __init__( self.characters = characters self.graphics_state = graphics_state self.k = k - self.url = url + self.link = link def __repr__(self): gstate = self.graphics_state.copy() @@ -43,7 +43,7 @@ def __repr__(self): del gstate["current_font"] # TMI return ( f"Fragment(characters={self.characters}," - f" graphics_state={gstate}, k={self.k}, url={self.url})" + f" graphics_state={gstate}, k={self.k}, link={self.link})" ) @property @@ -459,7 +459,7 @@ def get_line_of_given_width(self, maximum_width: float, wordsplit: bool = True): current_fragment.k, self.fragment_index, self.character_index, - current_fragment.url, + current_fragment.link, ) self.character_index += 1 diff --git a/test/links.pdf b/test/hyperlinks.pdf similarity index 63% rename from test/links.pdf rename to test/hyperlinks.pdf index e72b360d3..5f75b91ca 100644 Binary files a/test/links.pdf and b/test/hyperlinks.pdf differ diff --git a/test/internal_links.pdf b/test/internal_links.pdf new file mode 100644 index 000000000..87bf52572 Binary files /dev/null and b/test/internal_links.pdf differ diff --git a/test/link_to_other_document.pdf b/test/link_to_other_document.pdf new file mode 100644 index 000000000..98a46cbc3 Binary files /dev/null and b/test/link_to_other_document.pdf differ diff --git a/test/test_links.py b/test/test_links.py index 5772d6b27..ca37e6311 100644 --- a/test/test_links.py +++ b/test/test_links.py @@ -8,16 +8,14 @@ HERE = Path(__file__).resolve().parent -def test_links(tmp_path): +def test_hyperlinks(tmp_path): pdf = FPDF() pdf.add_page() pdf.set_font("helvetica", size=24) - line_height = 10 pdf.set_xy(80, 50) pdf.cell( - w=40, - h=line_height, + h=pdf.h, txt="Cell link", border=1, align="C", @@ -32,21 +30,13 @@ def test_links(tmp_path): width = pdf.get_string_width(text) pdf.link( x=80, - y=150 - line_height, + y=150 - pdf.h, w=width, - h=line_height, + h=pdf.h, link="https://github.com/PyFPDF/fpdf2", ) - pdf.add_page() - link = pdf.add_link() - pdf.set_link(link, page=1) - pdf.set_xy(50, 50) - pdf.cell( - w=100, h=10, txt="Internal link to first page", border=1, align="C", link=link - ) - - assert_pdf_equal(pdf, HERE / "links.pdf", tmp_path) + assert_pdf_equal(pdf, HERE / "hyperlinks.pdf", tmp_path) def test_link_alt_text(tmp_path): @@ -184,3 +174,104 @@ def test_later_call_to_set_link(tmp_path): # v2.6.1 bug spotted in discussion 7 pdf.cell(txt="Section 1: Bla bla bla") assert_pdf_equal(pdf, HERE / "later_call_to_set_link.pdf", tmp_path) + + +def test_link_to_other_document(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.set_font("helvetica", size=24) + + pdf.set_xy(80, 50) + pdf.cell( + txt="Link defined with FPDF.cell", + border=1, + align="C", + link="links.pdf", + ) + + pdf.set_y(100) + pdf.multi_cell( + w=pdf.epw, + txt="Link defined with FPDF.multi_cell", + border=1, + align="C", + link="links.pdf", + ) + + pdf.set_y(150) + pdf.multi_cell( + w=pdf.epw, + txt="Link defined with FPDF.multi_cell and markdown=True: [links.pdf](links.pdf)", + border=1, + align="C", + markdown=True, + ) + + pdf.set_xy(60, 200) + pdf.write_html('Link defined with FPDF.write_html') + + text = "Link defined with FPDF.link" + pdf.text(x=80, y=250, txt=text) + width = pdf.get_string_width(text) + pdf.link( + x=80, + y=250 - pdf.h, + w=width, + h=pdf.h, + link="links.pdf", + ) + + assert_pdf_equal(pdf, HERE / "link_to_other_document.pdf", tmp_path) + + +def test_internal_links(tmp_path): + pdf = FPDF() + pdf.set_font("helvetica", size=24) + + pdf.add_page() + pdf.y = 100 + pdf.cell(txt="Page 1", center=True) + + pdf.add_page() + + pdf.set_xy(80, 50) + pdf.cell( + txt="Link defined with FPDF.cell", + border=1, + align="C", + link=pdf.add_link(page=1), + ) + + pdf.set_y(100) + pdf.multi_cell( + w=pdf.epw, + txt="Link defined with FPDF.multi_cell", + border=1, + align="C", + link=pdf.add_link(page=1), + ) + + pdf.set_y(150) + pdf.multi_cell( + w=pdf.epw, + txt="Link defined with FPDF.multi_cell and markdown=True: [page 1](1)", + border=1, + align="C", + markdown=True, + ) + + pdf.set_xy(60, 200) + pdf.write_html('Link defined with FPDF.write_html') + + text = "Link defined with FPDF.link" + pdf.text(x=80, y=250, txt=text) + width = pdf.get_string_width(text) + pdf.link( + x=80, + y=250 - pdf.h, + w=width, + h=pdf.h, + link=pdf.add_link(page=1), + ) + + assert_pdf_equal(pdf, HERE / "internal_links.pdf", tmp_path)