Skip to content

Commit

Permalink
Review feedback and other fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
gmischler committed Oct 9, 2023
1 parent b94160b commit 9175d31
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 84 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,20 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
## [2.7.6] - Not released yet
This release is the first performed from the [@py-pdf GitHub org](https://github.com/py-pdf), where `fpdf2` migrated.
### Added
* The new experimental method `text_columns()` allows to render text within a single or multiple columns, including height balancing.
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now supports heading colors defined as attributes (_e.g._ `<h2 color="#00ff00">...`)
* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): Now supports padding in cells
* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): Now supports vertical alignment in cells
* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): Now supports outer border width for rendering the outer border of the table with a different line-width.
* documentation on how to use `livereload` to enable a "watch" mode with PDF generation: [Combine with livereload](https://py-pdf.github.io/fpdf2/CombineWithLivereload.html)
### Changed
* The formatting output by `write_html()` has changed in some aspects. Vertical spacing around headings and paragraphs may be slightly different, and elements at the top of the page don't have any extra spacing above anymore.
* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): If the height of a row is governed by an image, then the default vertical alignment of the other cells is "center". This was "top".
This change was made for consistency between row-height governed by text or images. The old behaviour can be enforced using the new vertical alignment parameter.
### Fixed
* In multi_cells and table cells with horizontal padding, the text was not given quite enough space.
* write_html() can now handle formatting tags within paragraphs without adding extra line breaks (except in table cells for now).
* the font size in HTML <pre> and <code> tags is not fixed to 11 pica anymore, but adapts to the preceding text.
* [`FPDF.ln()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.ln), when called before any text has been written, will now use the current font height instead of doing nothing - _cf._ issue [#937](https://github.com/py-pdf/fpdf2/issues/937)
* [`FPDF.image()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image), when provided a `BytesIO` instance, does not close it anymore - _cf._ issue [#881](https://github.com/py-pdf/fpdf2/issues/881)
* Invalid characters were being generated when a string contains parentheses - _cf._ issue [#884](https://github.com/py-pdf/fpdf2/issues/884)
Expand Down
83 changes: 51 additions & 32 deletions docs/TextColumns.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
_New in [:octicons-tag-24: 2.7.7](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
_New in [:octicons-tag-24: 2.7.6](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_

**Notice:** As of fpdf2 release 2.7.7, this is an experimental feature. Both the API and the functionality may change before it is finalized, without prior notice.
**Notice:** As of fpdf2 release 2.7.6, this is an experimental feature. Both the API and the functionality may change before it is finalized, without prior notice.


## Text Columns ##
Expand All @@ -11,60 +11,79 @@ Beyond the parameters common to all text regions, the following are available fo

* l_margin (float, optional) - override the current left page margin.
* r_margin (float, optional) - override the current right page margin.

Only for `FPDF.text_columns()`:

* ncols (float, optional) - the number of columns to generate (Default: 2).
* gutter (float, optional) - the space required between each two columns (Default 10).
* gutter (float, optional) - the horizontal space required between each two columns (Default 10).


#### Single-Column Example ####

In this example an inserted paragraph is used in order to format its content with justified alignment, while the rest of the text uses the default left alignment.

```python
cols = pdf.text_columns()
with cols:
cols.write(txt=LOREM_IPSUM)
with cols.paragraph(align="J") as par:
par.write(txt=LOREM_IPSUM[:100])
cols.write(txt=LOREM_IPSUM)
from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=12)

cols = pdf.text_columns()
with cols:
cols.write(text=LOREM_IPSUM[:400])
with cols.paragraph(
text_align="J",
top_margin=pdf.font_size,
bottom_margin=pdf.font_size
) as par:
par.write(text=LOREM_IPSUM[:400])
cols.write(text=LOREM_IPSUM[:400])
```
![Single Text Column](tcols-single.png)

#### Multi-Column Example

Here we have a layout with three columns. Note that font type and text size can be varied within a text region, while still maintaining the justified (in this case) horizontal alignment.

```python
with pdf.text_columns(align="J", ncols=3, gutter=5) as cols
cols.write(txt=LOREM_IPSUM)
pdf.set_font("Times", "", 8)
cols.write(txt=LOREM_IPSUM)
pdf.set_font("Courier", "", 10)
cols.write(txt=LOREM_IPSUM)
pdf.set_font("Helvetica", "", 12)
from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", size=16)

with pdf.text_columns(text_align="J", ncols=3, gutter=5) as cols:
cols.write(text=LOREM_IPSUM[:600])
pdf.set_font("Times", "", 18)
cols.write(text=LOREM_IPSUM[:500])
pdf.set_font("Courier", "", 20)
cols.write(text=LOREM_IPSUM[:500])
```
![Three Text Columns](tcols-three.png)

#### Balanced Columns

Normally the columns will be filled left to right, and if the text ends before the page is full, the rightmost column will be shorter than the others.
If you prefer that all columns on a page end on the same height, you can use the `balance=True` argument. In that case a simple algorithm will be applied that attempts to approximately balance their bottoms.

```python
cols = pdf.text_columns(align="J", ncols=3, gutter=5, balanced=True)
# fill columns with balanced text
with cols:
pdf.set_font("Times", "", 14)
cols.write(txt=LOREM_IPSUM[:300])
pdf.ln()
# add an image below
img_info = pdf.image("image_spanning_the_page_width.png")
# move vertical position to below the image
pdf.ln(img_info.rendered_hight + pdf.font_size)
# continue multi-column text
with cols:
cols.write(txt=LOREM_IPSUM[300:600])
from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=12)

cols = pdf.text_columns(text_align="J", ncols=3, gutter=5, balance=True)
# fill columns with balanced text
with cols:
pdf.set_font("Times", "", 14)
cols.write(text=LOREM_IPSUM[:300])
# add an image below
img_info = pdf.image(".../fpdf2/docs/regular_polygon.png",
x=pdf.l_margin, w=pdf.epw)
# continue multi-column text
with cols:
cols.write(text=LOREM_IPSUM[300:600])
```
![Balanced Columns](tcols-balanced.png)

Note that column balancing only works reliably when the font size (specifically the line height) doesn't change. If parts of the text use a larger or smaller font than the rest, then the balancing will usually be out of whack. Contributions for a more refined balancing algorithm are welcome.

Expand Down
21 changes: 11 additions & 10 deletions docs/TextRegion.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
_New in [:octicons-tag-24: 2.7.7](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
_New in [:octicons-tag-24: 2.7.6](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
# Text Flow Regions #

**Notice:** As of fpdf2 release 2.7.7, this is an experimental feature. Both the API and the functionality may change before it is finalized, without prior notice.
**Notice:** As of fpdf2 release 2.7.6, this is an experimental feature. Both the API and the functionality may change before it is finalized, without prior notice.

Text regions are a hierarchy of classes that enable to flow text within a given outline. In the simplest case, it is just the running text column of a page. But it can also be a sequence of outlines, such as several parallel columns or the cells of a table. Other outlines may be combined by addition or subtraction to create more complex shapes.

There are two general categories of regions. One defines boundaries for running text that will just continue in the same manner one the next page. Those include columns and tables. The second category are distinct shapes. Examples would be a circle, a rectangle, a polygon of individual shape or even an image. They may be used individually, in combination, or to modify the outline of a multipage column. Shape regions will typically not cause a page break when they are full. In the future, a possibility to chain them may be implemented, so that a new shape will continue with the text that didn't fit into the previous one.

**The current implementation only supports columns.** Table cells, shaped regions and combinations are still in the design phase.
The currently implemented text regions are:
* [Text Columns](TextColumns.html)

Other types like Table cells, shaped regions and combinations are still in the design phase, see [Quo vadis, .write()?](https://github.com/py-pdf/fpdf2/discussions/339).


## General Operation ##

Using the different region types and combination always follows the same pattern. The main difference to the normal `FPDF.write()` method is that all added text will first be buffered, and only gets rendered on the page when the context of the region is closed. This is necessary so that text can be aligned within the given boundaries even if its font, style, or size are arbitrarily varied along the way.

* Create the region instance with an `FPDF` method.
* Create the region instance with an `FPDF` method, , for example [text_columns()](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.text_columns).
<!--
* future: (_If desired, add or subtract other shapes from it (with geometric regions)_).
-->
* Use the `.write()` method of this text region in order to feed text into its buffer.
* Best practise is to use the region instance as a context manager for filling.
* Text will be rendered automatically after closing the context.
* When used as a context manager, you can change all text styling parameters within that context, and they will be used by the added text, but won't leak to the surroundings
* Text will be rendered automatically after closing the context.
* When used as a context manager, you can change all text styling parameters within that context, and they will be used by the added text, but won't leak to the surroundings
* Alternatively, eg. for filling a single column of text with the already existing settings, just use the region instance as is. In that case, you'll have to explicitly use the `render()` method after adding the text.
* Within a region, paragraphs can be inserted. The primary purpose of a paragraph is to apply a different horizontal alignment than the surrounding text. It is also possible to apply margins to the top and bottom of each paragraph.

Expand Down Expand Up @@ -51,7 +54,7 @@ Several region instances can exist at the same time. But only one of them can ac
All types of text regions have the following constructor parameters in common:

* text (str, optional) - text content to add to the region. This is a convenience parameter for cases when all text is available in one piece, and no partition into paragraphs (possibly with different parameters) is required. (Default: None)
* align (Align/str, optional) - the horizontal alignment of the text in the region. (Default: Align.L)
* text_align (Align/str, optional) - the horizontal alignment of the text in the region. (Default: Align.L)
* line_height (float, optional) - This is a factor by which the line spacing will be different from the font height. It works similar to the attribute of the same name in HTML/CSS. (default: 1.0)
* print_sh (bool, optional) - Treat a soft-hyphen (\\u00ad) as a printable character, instead of a line breaking opportunity. (Default: False)
* skip_leading_spaces (default: False) - This flag is primarily used by `write_html()`, but may also have other uses. It removes all space characters at the beginning of each line.
Expand All @@ -72,11 +75,9 @@ All of those values can be overriden for each individual paragraph.

The primary purpose of paragraphs is to enable variations in horizontal text alignment, while the horizontal extents of the text are managed by the text region. To set the alignment, you can use the `align` argument when creating the paragraph. Valid values are defined in the [`Align enum`](https://py-pdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.Align).

Note that the `write()` methods of paragraphs and text regions in general don't accept any other argument than "text" and "link".

For more typographical control, you can use the following arguments. Most of those override the settings of the current region when set, and default to the value set there.

* align (Align, optional) - The horizontal alignment of the paragraph.
* text_align (Align, optional) - The horizontal alignment of the paragraph.
* line_height (float, optional) - factor by which the line spacing will be different from the font height. (default: by region)
* top_margin (float, optional) - how much spacing is added above the paragraph. No spacing will be added at the top of the paragraph if the current y position is at (or above) the top margin of the page. (Default: 0.0)
* bottom_margin (float, optional) - Those two values determine how much spacing is added below the paragraph. No spacing will be added at the bottom if it would result in overstepping the bottom margin of the page. (Default: 0.0)
Expand Down
Binary file added docs/tcols-balanced.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/tcols-single.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/tcols-three.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion fpdf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from .html import HTMLMixin, HTML2FPDF
from .prefs import ViewerPreferences
from .template import Template, FlexTemplate
from .text_region import TextColumns
from .deprecation import WarnOnDeprecatedModuleAttributes

FPDF_VERSION = _FPDF_VERSION
Expand Down
7 changes: 4 additions & 3 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3748,7 +3748,7 @@ def text_columns(
ncols: int = 1,
gutter: float = 10,
balance: bool = False,
align: Union[Align, str] = "LEFT",
text_align: Union[Align, str] = "LEFT",
line_height: float = 1,
l_margin: float = None,
r_margin: float = None,
Expand All @@ -3763,7 +3763,8 @@ def text_columns(
gutter (float, optional): The distance between the columns. (Default: 10).
balance: (bool, optional): Specify whether multiple columns should end at approximately
the same height, if they don't fill the page. (Default: False)
align (Align or str, optional): The alignment of the region. (Default: "LEFT")
text_align (Align or str, optional): The alignment of the text within the region.
(Default: "LEFT")
line_height (float, optional): A multiplier relative to the font size changing the
vertical space occupied by a line of text. (Default: 1.0).
l_margin (float, optional): Override the current left page margin.
Expand All @@ -3781,7 +3782,7 @@ def text_columns(
ncols=ncols,
gutter=gutter,
balance=balance,
align=align,
text_align=text_align,
line_height=line_height,
l_margin=l_margin,
r_margin=r_margin,
Expand Down
2 changes: 1 addition & 1 deletion fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ def _new_paragraph(
if not top_margin and not self.follows_heading:
top_margin = self.font_size / self.pdf.k
self._paragraph = self._column.paragraph(
align=align,
text_align=align,
line_height=line_height,
skip_leading_spaces=True,
top_margin=top_margin,
Expand Down
5 changes: 3 additions & 2 deletions fpdf/line_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

from typing import NamedTuple, Any, Optional, Union, Sequence
from numbers import Number

from .enums import CharVPos, WrapMode, Align
from .errors import FPDFException
Expand Down Expand Up @@ -519,8 +520,8 @@ def __init__(
applicable width for the line with the given height at the current
vertical position. The height is relevant in those cases where the
lateral boundaries of the enclosing TextRegion() are not vertical.
margins (sequence of floats): The extra clearance (usually FPDF.c_margin)
that may apply at the beginning and/or end of a line.
margins (sequence of floats): The extra clearance that may apply at the beginning
and/or end of a line (usually either FPDF.c_margin or 0.0 for each side).
align (Align): The horizontal alignment of the current text block.
print_sh (bool): If True, a soft-hyphen will be rendered
normally, instead of triggering a line break. Default: False
Expand Down
Loading

0 comments on commit 9175d31

Please sign in to comment.