Skip to content

Commit

Permalink
Added support for CSS page breaks properties
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Jun 17, 2024
1 parent 68cf594 commit 3b02335
Show file tree
Hide file tree
Showing 9 changed files with 71 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
* feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving text shaping results
* [`FPDF.image()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): now handles `keep_aspect_ratio` in combination with an enum value provided to `x`
* file names are mentioned in errors when `fpdf2` fails to parse a SVG image
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): now supports CSS page breaks properties : [documentation](https://py-pdf.github.io/fpdf2/HTML.html#page-breaks)
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): spacing before lists can now be adjusted via the `HTML2FPDF.list_vertical_margin` attribute
### Fixed
* [`FPDF.local_context()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.local_context) used to leak styling during page breaks, when rendering `footer()` & `header()`
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pip install git+https://github.com/py-pdf/fpdf2.git@master
* Optional basic Markdown-like styling: `**bold**, __italics__`
* Can render [mathematical equations & charts](https://py-pdf.github.io/fpdf2/Maths.html)
* Usage examples with [Django](https://www.djangoproject.com/), [Flask](https://flask.palletsprojects.com), [FastAPI](https://fastapi.tiangolo.com/), [streamlit](https://streamlit.io/), AWS lambdas... : [Usage in web APIs](https://py-pdf.github.io/fpdf2/UsageInWebAPI.html)
* 1000+ unit tests running under Linux & Windows, with `qpdf`-based PDF diffing, timing & memory usage checks, and a high code coverage
* more than 1300 unit tests running under Linux & Windows, with `qpdf`-based PDF diffing, timing & memory usage checks, and a high code coverage

Our 350+ reference PDF test files, generated by `fpdf2`, are validated using 3 different checkers:

Expand Down
15 changes: 14 additions & 1 deletion docs/HTML.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pdf.write_html("""
<p><b>Hello</b> world. <u>I am</u> <i>tired</i>.</p>
<p><a href="https://github.com/py-pdf/fpdf2">py-pdf/fpdf2 GitHub repo</a></p>
<p align="right">right aligned text</p>
<p>i am a paragraph <br />in two parts.</p>
<p>i am a paragraph <br>in two parts.</p>
<font color="#00ff00"><p>hello in green</p></font>
<font size="7"><p>hello small</p></font>
<font face="helvetica"><p>hello helvetica</p></font>
Expand Down Expand Up @@ -124,6 +124,7 @@ pdf.output("html_dd_indented.pdf")

* `<h1>` to `<h8>`: headings (and `align` attribute)
* `<p>`: paragraphs (and `align`, `line-height` attributes)
* `<br>` & `<hr>` tags
* `<b>`, `<i>`, `<u>`: bold, italic, underline
* `<font>`: (and `face`, `size`, `color` attributes)
* `<center>` for aligning
Expand All @@ -141,6 +142,18 @@ pdf.output("html_dd_indented.pdf")
+ `<th>`: heading cells (with `align`, `bgcolor`, `width` attributes)
* `<td>`: cells (with `align`, `bgcolor`, `width`, `rowspan`, `colspan` attributes)

### Page breaks
Page breaks can be triggered manually using [page-break-before](https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before) or [page-break-after](https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-after).
For exemple you can use:
```html
<br style="page-break-after: always">
```
or:
```html
<p style="page-break-before: always">
Top of a new page.
</p>
```

## Known limitations

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Go try it **now** online in a Jupyter notebook: [![Open In Colab](https://colab.
* It has very few dependencies: [Pillow](https://pillow.readthedocs.io/en/stable/), [defusedxml](https://pypi.org/project/defusedxml/), & [fonttools](https://pypi.org/project/fonttools/)
* Can render [mathematical equations & charts](https://py-pdf.github.io/fpdf2/Maths.html)
* Many example scripts available throughout this documentation, including usage examples with [Django](https://www.djangoproject.com/), [Flask](https://flask.palletsprojects.com), [FastAPI](https://fastapi.tiangolo.com/), [streamlit](https://streamlit.io/), AWS lambdas... : [Usage in web APIs](UsageInWebAPI.md)
* Unit tests with `qpdf`-based PDF diffing, and PDF samples validation using 3 different checkers:
* more than 1300 unit tests with `qpdf`-based PDF diffing, and PDF samples validation using 3 different checkers:

[![QPDF logo](qpdf-logo.svg)](https://github.com/qpdf/qpdf)
[![PDF Checker logo](pdfchecker-logo.png)](https://www.datalogics.com/products/pdf-tools/pdf-checker/)
Expand Down
14 changes: 7 additions & 7 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2807,7 +2807,7 @@ def _start_local_context(
self,
font_family=None,
font_style=None,
font_size=None,
font_size_pt=None,
line_width=None,
draw_color=None,
fill_color=None,
Expand All @@ -2819,11 +2819,11 @@ def _start_local_context(
This method starts a "q/Q" context in the page content stream,
and inserts operators in it to initialize all the PDF settings specified.
"""
if "font_size_pt" in kwargs:
if font_size is not None:
if "font_size" in kwargs:
if font_size_pt is not None:
raise ValueError("font_size & font_size_pt cannot be both provided")
font_size = kwargs["font_size_pt"] / self.k
del kwargs["font_size_pt"]
font_size_pt = kwargs["font_size"] * self.k
del kwargs["font_size"]
gs = None
for key, value in kwargs.items():
if key in (
Expand Down Expand Up @@ -2868,11 +2868,11 @@ def _start_local_context(
else:
self._out("q")
# All the following calls to .set*() methods invoke .out() and write to the stream buffer:
if font_family is not None or font_style is not None or font_size is not None:
if font_family is not None or font_style is not None or font_size_pt is not None:
self.set_font(
font_family or self.font_family,
font_style or self.font_style,
font_size or self.font_size_pt,
font_size_pt or self.font_size_pt,
)
if line_width is not None:
self.set_line_width(line_width)
Expand Down
19 changes: 18 additions & 1 deletion fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def __init__(
self.emphasis = dict(b=False, i=False, u=False)
self.font_size = pdf.font_size_pt
self.set_font(pdf.font_family or "times", size=self.font_size, set_default=True)

self._page_break_after_paragraph = False
self._pre_formatted = False # preserve whitespace while True.
# nothing written yet to <pre>, remove one initial nl:
self._pre_started = False
Expand Down Expand Up @@ -464,6 +464,10 @@ def _end_paragraph(self):
self._column.render()
self._paragraph = None
self.follows_trailing_space = True
if self._page_break_after_paragraph:
# pylint: disable=protected-access
self.pdf._perform_page_break()
self._page_break_after_paragraph = False

def _write_paragraph(self, text, link=None):
if not self._paragraph:
Expand Down Expand Up @@ -537,6 +541,8 @@ def handle_data(self, data):
else:
self._write_data(data)
self.follows_trailing_space = data[-1] == " "
if self._page_break_after_paragraph:
self._end_paragraph()

def _write_data(self, data):
if self.href:
Expand All @@ -558,6 +564,10 @@ def handle_starttag(self, tag, attrs):
LOGGER.debug("STARTTAG %s %s", tag, attrs)
parse_style(attrs)
self._tags_stack.append(tag)
if attrs.get("page-break-before") == "always":
self._end_paragraph()
# pylint: disable=protected-access
self.pdf._perform_page_break()
if tag == "dt":
self._new_paragraph(
line_height=(
Expand Down Expand Up @@ -891,6 +901,13 @@ def handle_starttag(self, tag, attrs):
self.pdf.char_vpos = "SUP"
if tag == "sub":
self.pdf.char_vpos = "SUB"
if attrs.get("page-break-after") == "always":
if tag in ("br", "hr", "img"):
self._end_paragraph()
# pylint: disable=protected-access
self.pdf._perform_page_break()
else:
self._page_break_after_paragraph = True

def handle_endtag(self, tag):
LOGGER.debug("ENDTAG %s", tag)
Expand Down
Binary file added test/html/html_page_break_after.pdf
Binary file not shown.
Binary file added test/html/html_page_break_before.pdf
Binary file not shown.
29 changes: 29 additions & 0 deletions test/html/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,32 @@ def test_html_list_vertical_margin(tmp_path):
"""
pdf.write_html(html, list_vertical_margin=margin_value)
assert_pdf_equal(pdf, HERE / "html_list_vertical_margin.pdf", tmp_path)


def test_html_page_break_before(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""Content on first page.
<br style="page-break-before: always">
Content on second page.
<p style="page-break-before: always">
Content on third page.
</p>"""
)
assert_pdf_equal(pdf, HERE / "html_page_break_before.pdf", tmp_path)


def test_html_page_break_after(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""Content on first page.
<br style="page-break-after: always">
Content on second page.
<p style="page-break-after: always">
Other content on second page.
</p>
Content on third page."""
)
assert_pdf_equal(pdf, HERE / "html_page_break_after.pdf", tmp_path)

0 comments on commit 3b02335

Please sign in to comment.