Skip to content

Commit 2b70b6d

Browse files
committed
review feedback & additional tests
1 parent 3a16c14 commit 2b70b6d

11 files changed

+238
-112
lines changed

docs/TextRegion.md

+32-14
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
1+
_New in [:octicons-tag-24: 2.7.7](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
12
# Text Flow Regions #
2-
_New in [:octicons-tag-24: 2.7.6](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
33

4-
**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.
4+
**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.
55

66
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.
77

88
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.
99

10+
**The current implementation only supports columns.** Shaped regions and combinations are still in the design phase.
11+
1012
## General Operation ##
1113

1214
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 you need to explicitly trigger its rendering on the page. 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.
1315

1416
* Create the region instance with an `FPDF` method.
15-
* If desired, add or subtract other shapes from it (with geometric regions).
17+
* future: (_If desired, add or subtract other shapes from it (with geometric regions)_).
1618
* Use the `.write()` method to feed text into its buffer.
1719
* Best practise is to use the region instance as a context manager for filling.
1820
* Text will be rendered automatically after closing the context.
1921
* 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
20-
* For adding 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.
22+
* 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.
2123
* Within a region, paragraphs can be inserted. The primary purpose of a paragraph is to apply a different horizontal alignment than the surrounding text.
2224

2325
### Text Start Position ###
2426

25-
When rendering, the vertical start position of the text will be at the lowest one out of the current y position, the top of the region (if it has a defined top), or the top margin of the page. The horizontal start position will either at the current x position or at the left edge of the region, whichever is further to the right. In both horizontal and vertical positioning, regions with multiple columns may follow additional rules and restrictions.
27+
When rendering, the vertical start position of the text will be at the lowest one out of the current y position, the top of the region (if it has a defined top), or the top margin of the page. The horizontal start position will be either at the current x position if that lies within the boundaries of the region/column or at the left edge of the region. In both horizontal and vertical positioning, regions with multiple columns may follow additional rules and restrictions.
2628

2729

2830
### Interaction between Regions ###
2931

30-
Several region instances can exist at the same time. But only one of them can act as context manager at any given time. It is not currently possible to operate them recursively.
31-
But it is possible to use them intermittingly. This will probably most often make sense between a columnar region and a table. You may have some running text ending at a given height, then insert a table with data, and finally continue the running text at the new height below the table within the existing column.
32+
Several region instances can exist at the same time. But only one of them can act as context manager at any given time. It is not currently possible to use them recursively.
33+
But it is possible to use them intermittingly. This will probably most often make sense between a columnar region and a table. You may have some running text ending at a given height, then insert a table with data, and finally continue the running text at the new height below the table within the existing column(s).
3234

3335

3436
## Columns ##
@@ -53,8 +55,7 @@ In this example an inserted paragraph is used in order to format its content wit
5355
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.
5456

5557
```python
56-
cols = pdf.text_columns(align="J", ncols=3, gutter=5)
57-
with cols:
58+
with pdf.text_columns(align="J", ncols=3, gutter=5) as cols
5859
cols.write(txt=LOREM_IPSUM)
5960
pdf.set_font("Times", "", 8)
6061
cols.write(txt=LOREM_IPSUM)
@@ -69,11 +70,22 @@ Normally the columns will be filled left to right, and if the text ends before t
6970
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.
7071

7172
```python
72-
with pdf.text_columns(align="J", ncols=3, gutter=5, balanced=True) as cols:
73+
cols = pdf.text_columns(align="J", ncols=3, gutter=5, balanced=True)
74+
# fill columns with balanced text
75+
with cols:
7376
pdf.set_font("Times", "", 14)
7477
cols.write(txt=LOREM_IPSUM[:300])
78+
pdf.ln()
79+
# add an image below
80+
img_info = pdf.image("image_spanning_the_page_width.png")
81+
# move vertical position to below the image
82+
pdf.ln(img_info.rendered_hight + pdf.font_size)
83+
# continue multi-column text
84+
with cols:
85+
cols.write(txt=LOREM_IPSUM[300:600])
7586
```
76-
Note that this 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.
87+
88+
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.
7789

7890
### Possible future extensions
7991

@@ -84,13 +96,19 @@ Those features are currently not supported, but Pull Requests are welcome to imp
8496

8597
## Paragraphs ##
8698

87-
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.
99+
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, with the same `Align` values as elsewhere in the library. Note that the `write()` methods of paragraphs and text regions in general don't accept this argument, they only accept text.
100+
101+
For more typographical control, you can also use the following arguments:
102+
* line_height (default: 1.0) - 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.
103+
* top_margin (default: 0.0)
104+
* bottom_margin (default: 0.0) - Those two values determine how much spacing is added above and below the paragraph. No spacing will be added at the top if the paragraph if the current y position is at (or above) the top margin of the page. Similarly, none will be added at the bottom if it would result in overstepping the bottom margin of the page.
105+
* 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.
88106

89107
Other than text regions, paragraphs should alway be used as context managers and never be reused. Violating those rules may result in the entered text turning up on the page out of sequence.
90108

91109
### Possible future extensions
92110

93111
Those features are currently not supported, but Pull Requests are welcome to implement them:
94112

95-
* Setting the spacing between paragraphs
96-
* first-line indent
113+
* per-paragraph indentation
114+
* first-line indentation

fpdf/fpdf.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -3409,6 +3409,7 @@ def multi_cell(
34093409
"""
34103410

34113411
padding = Padding.new(padding)
3412+
wrapmode = WrapMode.coerce(wrapmode)
34123413

34133414
if split_only:
34143415
warnings.warn(
@@ -3443,7 +3444,6 @@ def multi_cell(
34433444
)
34443445
if not self.font_family:
34453446
raise FPDFException("No font set, you need to call set_font() beforehand")
3446-
wrapmode = WrapMode.coerce(wrapmode)
34473447
if isinstance(w, str) or isinstance(h, str):
34483448
raise ValueError(
34493449
# pylint: disable=implicit-str-concat
@@ -3746,6 +3746,7 @@ def text_column(
37463746
l_margin: float = None,
37473747
r_margin: float = None,
37483748
print_sh: bool = False,
3749+
wrapmode: WrapMode = WrapMode.WORD,
37493750
skip_leading_spaces: bool = False,
37503751
):
37513752
"""Establish a layout with a single column to fill with text.
@@ -3770,6 +3771,7 @@ def text_column(
37703771
l_margin=l_margin,
37713772
r_margin=r_margin,
37723773
print_sh=print_sh,
3774+
wrapmode=wrapmode,
37733775
skip_leading_spaces=skip_leading_spaces,
37743776
)
37753777

@@ -3785,6 +3787,7 @@ def text_columns(
37853787
l_margin: float = None,
37863788
r_margin: float = None,
37873789
print_sh: bool = False,
3790+
wrapmode: WrapMode = WrapMode.WORD,
37883791
skip_leading_spaces: bool = False,
37893792
):
37903793
"""Establish a layout with multiple columns to fill with text.
@@ -3813,6 +3816,7 @@ def text_columns(
38133816
l_margin=l_margin,
38143817
r_margin=r_margin,
38153818
print_sh=print_sh,
3819+
wrapmode=wrapmode,
38163820
skip_leading_spaces=skip_leading_spaces,
38173821
)
38183822

fpdf/html.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ def handle_data(self, data):
392392
# ignore anything else than td inside a table
393393
pass
394394
elif self._pre_formatted: # pre blocks
395+
# If we want to mimick the exact HTML semantics about newlines at the
396+
# beginning and end of the block, then this needs some more thought.
395397
s_nl = data.startswith("\n") and self._pre_started
396398
self._pre_started = False
397399
e_nl = data.endswith("\n")
@@ -522,11 +524,9 @@ def handle_starttag(self, tag, attrs):
522524
self.font_color = color
523525
if "face" in attrs:
524526
face = attrs.get("face").lower()
525-
try:
526-
self.set_font(face)
527-
self.font_face = face
528-
except RuntimeError:
529-
pass # font not found, ignore
527+
# This may result in a FPDFException "font not found".
528+
self.set_font(face)
529+
self.font_face = face
530530
if "size" in attrs:
531531
self.font_size = int(attrs.get("size"))
532532
self.set_font()

0 commit comments

Comments
 (0)