Skip to content

Commit

Permalink
html via text regions first round
Browse files Browse the repository at this point in the history
  • Loading branch information
gmischler committed Sep 24, 2023
1 parent 186521e commit 7c7f77e
Show file tree
Hide file tree
Showing 23 changed files with 374 additions and 226 deletions.
23 changes: 12 additions & 11 deletions docs/TextRegion.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# Text Flow Regions #
_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.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.
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.

## 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 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.

* Create the region instance
* If desired, add or subtract other shapes from it
* Use the `.write()` method to feed text into its buffer
* You can use the region instance as a context manager for filling, but you don't have to
* 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
* For adding text with the already existing settings, just use the region instance as is.
* Create the region instance with an `FPDF` method.
* If desired, add or subtract other shapes from it (with geometric regions).
* Use the `.write()` method 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
* 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.
* Within a region, paragraphs can be inserted. The primary purpose of a paragraph is to apply a different horizontal alignment than the surrounding text.
* Once all the desired text is collected to fill a shape or a set of columns, you can call its `.render()` method to actually do so.


### Text Start Position ###

Expand All @@ -30,6 +30,7 @@ When rendering, the vertical start position of the text will be at the lowest on
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.
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.


## Columns ##

The `FPDF.text_column() and ``FPDF.text_columns()` methods allow to create columnar layouts, with one or several columns respectively. Columns will always be of equal width.
Expand Down Expand Up @@ -64,8 +65,8 @@ Here we have a layout with three columns. Note that font type and text size can

#### 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 end up shorter than the others.
If you prefer that all columns on a page end on the same height, you can use the `balanced=True` argument. In that case a simple algorithm will be applied that attempts to approximately balance their bottoms.
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
with pdf.text_columns(align="J", ncols=3, gutter=5, balanced=True) as cols:
Expand Down
28 changes: 21 additions & 7 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2922,6 +2922,8 @@ def _render_styled_text_line(
current_text_mode = self.text_mode
current_font_stretching = self.font_stretching
current_char_spacing = self.char_spacing
fill_color_changed = False
last_used_color = self.fill_color
if text_line.fragments:
if text_line.align == Align.R:
dx = w - self.c_margin - styled_txt_width
Expand All @@ -2930,10 +2932,6 @@ def _render_styled_text_line(
else:
dx = self.c_margin
s_start += dx

if self.fill_color != self.text_color:
sl.append(self.text_color.serialize().lower())

word_spacing = 0
if text_line.align == Align.J and text_line.number_of_spaces:
word_spacing = (
Expand All @@ -2944,6 +2942,11 @@ def _render_styled_text_line(
f"{(self.h - self.y - 0.5 * h - 0.3 * max_font_size) * k:.2f} Td"
)
for i, frag in enumerate(text_line.fragments):
if frag.graphics_state["text_color"] != last_used_color:
# allow to change color within the line of text.
last_used_color = frag.graphics_state["text_color"]
sl.append(last_used_color.serialize().lower())
fill_color_changed = True
if word_spacing and frag.font_stretching != 100:
# Space character is already stretched, extra spacing is absolute.
frag_ws = word_spacing * 100 / frag.font_stretching
Expand Down Expand Up @@ -3044,7 +3047,7 @@ def _render_styled_text_line(
or current_font != self.current_font
or current_font_size_pt != self.font_size_pt
or current_text_mode != self.text_mode
or self.fill_color != self.text_color
or fill_color_changed
or current_font_stretching != self.font_stretching
or current_char_spacing != self.char_spacing
):
Expand All @@ -3065,7 +3068,10 @@ def _render_styled_text_line(
elif new_x == XPos.END:
self.x = s_start + s_width
elif new_x == XPos.WCONT:
self.x = s_start + s_width - self.c_margin
if s_width:
self.x = s_start + s_width - self.c_margin
else:
self.x = s_start
elif new_x == XPos.CENTER:
self.x = s_start + s_width / 2.0
elif new_x == XPos.LMARGIN:
Expand Down Expand Up @@ -3708,6 +3714,7 @@ def text_column(
l_margin: float = None,
r_margin: float = None,
print_sh: bool = False,
skip_leading_spaces: bool = False,
):
"""Establish a layout with a single column to fill with text.
Args:
Expand All @@ -3719,6 +3726,8 @@ def text_column(
r_margin (float, optional): Override the current right page margin.
print_sh (bool, optional): Treat a soft-hyphen (\\u00ad) as a printable
character, instead of a line breaking opportunity. Default value: False
skip_leading_spaces (bool, optional): On each line, any space characters
at the beginning will be skipped. Default value: False.
"""
return TextColumns(
self,
Expand All @@ -3729,6 +3738,7 @@ def text_column(
l_margin=l_margin,
r_margin=r_margin,
print_sh=print_sh,
skip_leading_spaces=skip_leading_spaces,
)

@check_page
Expand All @@ -3743,19 +3753,22 @@ def text_columns(
l_margin: float = None,
r_margin: float = None,
print_sh: bool = False,
skip_leading_spaces: bool = False,
):
"""Establish a layout with multiple columns to fill with text.
Args:
text (str, optional): A first piece of text to insert.
ncols (int, optional): the number of columns to create, default 2.
gutter (float, optional): The distance between the columns, default 10.
gutter (float, optional): The distance between the columns, default 10 mm.
align (Align or str, optional): The alignment of 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.
r_margin (float, optional): Override the current right page margin.
print_sh (bool, optional): Treat a soft-hyphen (\\u00ad) as a printable
character, instead of a line breaking opportunity. Default value: False
skip_leading_spaces (bool, optional): On each line, any space characters
at the beginning will be skipped. Default value: False.
"""
return TextColumns(
self,
Expand All @@ -3768,6 +3781,7 @@ def text_columns(
l_margin=l_margin,
r_margin=r_margin,
print_sh=print_sh,
skip_leading_spaces=skip_leading_spaces,
)

@check_page
Expand Down
9 changes: 6 additions & 3 deletions fpdf/graphics_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ def __init__(self, *args, **kwargs):
]
super().__init__(*args, **kwargs)

def _push_local_stack(self):
self.__statestack.append(self.__statestack[-1].copy())
def _push_local_stack(self, new=None):
if new:
self.__statestack.append(new)
else:
self.__statestack.append(self.__statestack[-1].copy())

def _pop_local_stack(self):
del self.__statestack[-1]
return self.__statestack.pop()

def _get_current_graphics_state(self):
return self.__statestack[-1].copy()
Expand Down
Loading

0 comments on commit 7c7f77e

Please sign in to comment.