Skip to content

Commit

Permalink
Added support for CSS page break properties + bugfixes (#1209)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C authored Jun 18, 2024
1 parent 68cf594 commit ddc72ff
Show file tree
Hide file tree
Showing 28 changed files with 200 additions and 76 deletions.
19 changes: 10 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,21 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
## [2.7.10] - Not released yet
### Added
* [`Templates`](https://py-pdf.github.io/fpdf2/fpdf/Templates.html) can now be also defined in JSON files.
* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159)
* support for quadratic and cubic Bézier curves with [`FPDF.bezier()`](https://py-pdf.github.io/fpdf2/fpdf/Shapes.html#fpdf.fpdf.FPDF.bezier)
* feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving text shaping results
* support to optionally set `wrapmode` in templates (default `"WORD"` can optionally be set to `"CHAR"` to support wrapping on characters for scripts like Chinese or Japanese) - _cf._ [#1159](https://github.com/py-pdf/fpdf2/issues/1159) - thanks to @carlhiggs
* support for quadratic and cubic Bézier curves with [`FPDF.bezier()`](https://py-pdf.github.io/fpdf2/fpdf/Shapes.html#fpdf.fpdf.FPDF.bezier) - thanks to @awmc000
* feature to identify the Unicode script of the input text and break it into fragments when different scripts are used, improving [text shaping](https://py-pdf.github.io/fpdf2/TextShaping.html) 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): spacing before lists can now be adjusted via the `HTML2FPDF.list_vertical_margin` attribute
* [`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 - thanks to @lcgeneralprojects
### 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()`
* [`fpdf.drawing.DeviceCMYK`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceCMYK) objects can now be passed to [`FPDF.set_draw_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_draw_color), [`FPDF.set_fill_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_fill_color) and [`FPDF.set_text_color()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_text_color) without raising a `ValueError`: [documentation](https://py-pdf.github.io/fpdf2/Text.html#text-formatting).
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): fixing rendering of `<hr>` tags, that do not trigger a page break anymore
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): fixed automatic page break when an image does not have enough vertical space to be rendered on a page
* individual `/Resources` directories are now properly created for each document page. This change ensures better compliance with the PDF specification but results in a slight increase in the size of PDF documents. You can still use the old behavior by setting `FPDF().single_resources_object = True`
* line size calculation for fragments when text shaping is used
* fixed incoherent indentation of long list entries - _cf._ [issue #1073](https://github.com/py-pdf/fpdf2/issues/1073)
* line size calculation for fragments when [text shaping](https://py-pdf.github.io/fpdf2/TextShaping.html) is used
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): fixed incoherent indentation of long `<ul>` list entries - _cf._ [issue #1073](https://github.com/py-pdf/fpdf2/issues/1073) - thanks to @lcgeneralprojects
* default values for `top_margin` and `bottom_margin` in `HTML2FPDF._new_paragraph()` calls are now correctly converted into chosen document units.
### Removed
* an obscure and undocumented [feature](https://github.com/py-pdf/fpdf2/issues/1198) of [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html), which used to magically pass local variables as arguments.
Expand All @@ -56,17 +58,16 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
* SVG clipping paths being incorrectly painted - _cf._ [issue #1147](https://github.com/py-pdf/fpdf2/issues/1147)
* new translation of the tutorial in [Polski](https://py-pdf.github.io/fpdf2/Tutorial-pl.html) - thanks to @DarekRepos
### Changed
* improved the performance of `FPDF.start_section()` - _cf._ [issue #1092](https://github.com/py-pdf/fpdf2/issues/1092)
* improved the performance of `FPDF.start_section()` - thanks to @dmail00 - _cf._ [issue #1092](https://github.com/py-pdf/fpdf2/issues/1092)
### Deprecated
* The `dd_tag_indent` & `li_tag_indent` parameters of `FPDF.write_html()` are replaced by the new `tag_indents` generic parameter.
* The `heading_sizes` & `pre_code_font` parameters of `FPDF.write_html()` are replaced by the new `tag_styles` generic parameter.

## [2.7.8] - 2024-02-09
### Added
* support for `<path>` elements in SVG `<clipPath>` elements
* support for `bidirectional` text shaping - thanks to @andersonhc
* support for `bidirectional` [text shaping](https://py-pdf.github.io/fpdf2/TextShaping.html) - thanks to @andersonhc
* documentation on how to combine `fpdf2` with [mistletoe](https://pypi.org/project/kaleido/) in order to [generate PDF documents from Markdown (link)](https://py-pdf.github.io/fpdf2/CombineWithMistletoeoToUseMarkdown.html)
* tutorial in Dutch: [Handleiding](https://py-pdf.github.io/fpdf2/Tutorial-nl.md) - thanks to @Polderrider
* support for `Table` cells that span multiple rows via the `rowspan` attribute, which can be combined with `colspan` - thanks to @mjasperse
* `TableSpan.COL` and `TableSpan.ROW` enums that can be used as placeholder table entries to identify span extents - thanks to @mjasperse
### Fixed
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
8 changes: 8 additions & 0 deletions docs/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ assert_pdf_equal(
)
```

### Visually comparing all PDF reference files modified on a branch
This script will build an serve a single HTML page containing
all PDF references file modified on your current `git` branch,
and render them side by side with the PDF file from the `master` branch,
so that you can quickly scroll and check for visible differences:

scripts/compare-changed-pdfs.py

## Testing performances

### Code speed & profiling
Expand Down
19 changes: 17 additions & 2 deletions 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 @@ -79,7 +79,7 @@ pdf.output("html.pdf")
```


## Styling HTML tags globally
### Styling HTML tags globally

_New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

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,20 @@ 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
_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

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

## Known limitations

Expand Down
2 changes: 2 additions & 0 deletions docs/Shapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ pdf.output("solid_arc.pdf")
![](solid_arc.png)

## Bezier Curve ##
_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

Using [`bezier()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.bezier) to create a cubic Bezier curve:
```python
from fpdf import FPDF
Expand Down
2 changes: 2 additions & 0 deletions docs/Tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ The cell color is set following those settings, ordered by priority:
4. The table setting `cell_fill_color`, if `cell_fill_mode` indicates to fill a cell
5. The document `.fill_color` set before rendering the table

_New in [:octicons-tag-24: 2.7.9](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

Finally, it is possible to define your own cell-filling logic:

```python
Expand Down
1 change: 1 addition & 0 deletions docs/Templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ f.render("./template.pdf")
See template.py or [Web2Py] (Web2Py.md) for a complete example.

# Example - Elements defined in JSON file #
_New in [:octicons-tag-24: 2.7.10](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

The JSON file must consist of an array of objects.
Each object with its name/value pairs define a template element:
Expand Down
3 changes: 2 additions & 1 deletion docs/Text.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ For all text insertion methods, the relevant font related properties (eg. font/s
All three `set_*_colors()` methods accept either a single greyscale value, 3 values as RGB components, a single `#abc` or `#abcdef` hexadecimal color string, or an instance of [`fpdf.drawing.DeviceCMYK`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceCMYK), [`fpdf.drawing.DeviceRGB`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceRGB) or [`fpdf.drawing.DeviceGray`](https://py-pdf.github.io/fpdf2/fpdf/drawing.html#fpdf.drawing.DeviceGray).
You can even use [named web colors](https://en.wikipedia.org/wiki/Web_colors#HTML_color_names) by using [`html.color_as_decimal()`](fpdf/html.html#fpdf.html.color_as_decimal).

In addition, some of the methods can optionally use [markdown](TextStyling.md#markdowntrue) or [HTML](HTML.md) markup in the supplied text in order to change the font style (bold/italic/underline) of parts of the output.
More text styling options can be found on the page [Text styling](TextStyling.md),
including [Markdown syntax](TextStyling.md#markdowntrue) and [HTML markup](HTML.md).

## Change in current position
`.cell()` and `.multi_cell()` let you specify where the current position (`.x`/`.y`) should go after the call.
Expand Down
14 changes: 9 additions & 5 deletions docs/TextStyling.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Text styling #

## set_font() ##
## .set_font() ##

Setting emphasis on text can be controlled by using `set_font(style=...)`:
Setting emphasis on text can be controlled by using `.set_font(style=...)`:

* `style="B"` indicates **bold**
* `style="I"` indicates _italics_
Expand Down Expand Up @@ -66,6 +66,10 @@ pdf.multi_cell(w=150, text=LOREM_IPSUM[:200], new_x="LEFT", fill=True)
```
![](char_spacing.png)

For a more complete support of **Markdown** syntax,
check out this guide to combine `fpdf2` with the `mistletoe` library:
[Combine with mistletoe to use Markdown](CombineWithMistletoeoToUseMarkdown.md).


## Subscript, Superscript, and Fractional Numbers

Expand Down Expand Up @@ -175,7 +179,7 @@ More examples from [`test_text_mode.py`](https://github.com/py-pdf/fpdf2/blob/ma

An optional `markdown=True` parameter can be passed to the [`cell()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.cell)
& [`multi_cell()`](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) methods
in order to enable basic Markdown-like styling: `**bold**, __italics__, --underlined--`
in order to enable basic Markdown-like styling: `**bold**, __italics__, --underlined--`.

Bold & italics require using dedicated fonts for each style.

Expand All @@ -199,9 +203,9 @@ Several unit tests in `test/text/` demonstrate that:
* [test_multi_cell_markdown_with_ttf_fonts](https://github.com/py-pdf/fpdf2/blob/2.6.1/test/text/test_multi_cell_markdown.py#L27)


## write_html ##
## .write_html() ##

[`write_html`](HTML.md) allows to set emphasis on text through the `<b>`, `<i>` and `<u>` tags:
[`.write_html()`](HTML.md) allows to set emphasis on text through the `<b>`, `<i>` and `<u>` tags:

```python
pdf.write_html("""<B>bold</B>
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
32 changes: 23 additions & 9 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,7 @@ def local_context(self, **kwargs):
fill_opacity
font_family
font_size
font_size_pt
font_style
font_stretching
intersection_rule
Expand Down Expand Up @@ -2807,7 +2808,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 +2820,16 @@ 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:
# At some point we may want to deprecate font_size here in favour of font_size_pt,
# and raise a warning if font_size is provided:
# * font_size_pt is more consistent with the size parameter of .set_font(), provided in points.
# * font_size can be misused, as users may not be aware of the difference between the 2 properties,
# and may erroneously provide a value in points as font_size.
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 +2874,15 @@ 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 Expand Up @@ -3586,15 +3596,19 @@ def _perform_page_break(self):
# by popping out every GraphicsState:
gs_stack = []
while self._is_current_graphics_state_nested():
gs_stack.append(self._get_current_graphics_state())
self._pop_local_stack()
# This code assumes that every Graphics State in the stack
# has been pushed in it while adding a "q" in the PDF stream
# (which is what FPDF.local_context() does):
self._end_local_context()
gs_stack.append(self._pop_local_stack())
# Using a temporary GS to render header & footer:
self._push_local_stack()
self.add_page(same=True)
for prev_gs in reversed(gs_stack):
self._push_local_stack(prev_gs)
self._start_local_context(**prev_gs)
self._push_local_stack()
self._pop_local_stack()
self.x = x # restore x but not y after drawing header

def _has_next_page(self):
Expand Down
Loading

0 comments on commit ddc72ff

Please sign in to comment.