Skip to content

Commit

Permalink
Documenting links to other documents & pages (#823)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Jun 19, 2023
1 parent 4357991 commit 957c28e
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 30 deletions.
62 changes: 57 additions & 5 deletions docs/Links.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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: `<a href="page number">link text</a>`

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: `<a href="other_doc.pdf">link text</a>`

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 ##
Expand Down
16 changes: 11 additions & 5 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 "")
Expand Down
10 changes: 5 additions & 5 deletions fpdf/line_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,23 +27,23 @@ 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)
else:
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()
if "current_font" in gstate:
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
Expand Down Expand Up @@ -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
Expand Down
Binary file renamed test/links.pdf → test/hyperlinks.pdf
Binary file not shown.
Binary file added test/internal_links.pdf
Binary file not shown.
Binary file added test/link_to_other_document.pdf
Binary file not shown.
121 changes: 106 additions & 15 deletions test/test_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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):
Expand Down Expand Up @@ -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('<a href="links.pdf">Link defined with FPDF.write_html</a>')

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('<a href="1">Link defined with FPDF.write_html</a>')

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)

0 comments on commit 957c28e

Please sign in to comment.