Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of rowspan in tables #1088

Merged
merged 32 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
664b145
table: Initial functional implementation of rowspan
mjasperse Jan 27, 2024
20492da
table: Permit combination of colspan+rowspan
mjasperse Dec 23, 2023
e00cb1a
table: Introduced TableSpan enum placeholder for rowspan/colspan
mjasperse Jan 27, 2024
beec54f
table: WIP refactor of rowspan regularisation
mjasperse Jan 3, 2024
63abb7c
Docs: Added `rowspan` to tables page
mjasperse Dec 24, 2023
225b787
table: Fixed book-keeping for combined rowspan and colspan
mjasperse Dec 24, 2023
1407bf8
table: Dropped cols_count and column_indices from Row
mjasperse Dec 25, 2023
a69b35d
table: Fixed gutter accumulation at end of table
mjasperse Dec 25, 2023
eccd68b
html: Added cellpadding and cellspacing table attribute handling
mjasperse Dec 25, 2023
63525bd
table: refactored rowspan processing
mjasperse Jan 27, 2024
be0d2a9
table: Check for rowspans across heading boundary
mjasperse Jan 2, 2024
638ae3e
html: Fixed table border reset when no <th> present
mjasperse Jan 2, 2024
ab2733e
test: Adding test cases for rowspan implementation
mjasperse Jan 3, 2024
8b7f82e
table: Fixed handling of RHS border in case of colspan
mjasperse Jan 27, 2024
00aaebd
table: Fixed get_cell_border for rowspan
mjasperse Jan 27, 2024
deb18c2
table: Dropped requirement that all rows have the same number of columns
mjasperse Jan 3, 2024
34e4215
html: Handle border=0 in table definition
mjasperse Jan 27, 2024
4be92cc
table: Use single disable_writing call for consistency
mjasperse Jan 3, 2024
782e805
table: Fix calculation of row heights with images
mjasperse Jan 3, 2024
9828a08
table: Replaced assertion errors with FPDFException
mjasperse Jan 3, 2024
360221a
test: Added test case for varying number of table columns
mjasperse Jan 3, 2024
5e321e8
changelog: Updated for table changes
mjasperse Jan 27, 2024
2b13b72
doc: Updated docstring of fpdf.table()
mjasperse Jan 3, 2024
e8e3e21
table: Fixed indexing for combined rowspan and colspan
mjasperse Jan 3, 2024
d271a5b
table: Fix pagebreaks with overlapping spans and added test
mjasperse Jan 3, 2024
037da4e
table: minor refactor and blacked
mjasperse Jan 3, 2024
07486a2
table: Improved rowspan padding accumulation, added image test
mjasperse Jan 3, 2024
0c8f479
table: Minor changes per pylint suggestions
mjasperse Jan 27, 2024
54068ed
table: Fixed regression in pagebreak calculation
mjasperse Jan 3, 2024
e9dac08
html: Updated docs for new table attributes
mjasperse Jan 3, 2024
a708d2b
table: Fixed outer border handling after pagebreak
mjasperse Jan 27, 2024
b9a862c
Merge branch 'master' into feature/table_rowspan
andersonhc Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
* support for `<path>` elements in SVG `<clipPath>` elements
* 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
* when adding a link on a table cell, an extra link was added erroneously on the left. Moreover, now `FPDF._disable_writing()` properly disable link writing.
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now handles linking directly to other pages - thanks to @mjasperse
* non-bold `TitleStyle` is now rendered as non-bold even when the current font is bold
* calling `.table()` inside the `render_toc_function`
* using `.set_text_shaping(True)` & `.offset_rendering()`
* fixed gutter handing when a pagebreak occurs within a table with header rows - thanks to @mjasperse
* fixed handling of `border=0` in HTML table - thanks to @mjasperse
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now properly honors `align=` attributes in `<th>` tags
### Changed
* refactored [`FPDF.multi_cell()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) to generate fewer PDF component objects - thanks to @mjasperse
* outer table borders are now drawn continuously for nonzero `gutter_width`/`gutter_height`, with spacing applied inside the border similar to HTML tables - thanks to @mjasperse - cf. [#1071](https://github.com/py-pdf/fpdf2/issues/1071)
* removed the requirement that all rows in a `Table` have the same number of columns - thanks to @mjasperse

## [2.7.7] - 2023-12-10
### Added
Expand Down
4 changes: 2 additions & 2 deletions docs/HTML.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ pdf.output("html.pdf")
* `<ol>`, `<ul>`, `<li>`: ordered, unordered and list items (can be nested)
* `<dl>`, `<dt>`, `<dd>`: description list, title, details (can be nested)
* `<sup>`, `<sub>`: superscript and subscript text
* `<table>`: (with `align`, `border`, `width` attributes)
* `<table>`: (with `align`, `border`, `width`, `cellpadding`, `cellspacing` attributes)
+ `<thead>`: optional tag, wraps the table header row
+ `<tfoot>`: optional tag, wraps the table footer row
+ `<tbody>`: optional tag, wraps the table rows with actual content
+ `<tr>`: rows (with `align`, `bgcolor` attributes)
+ `<th>`: heading cells (with `align`, `bgcolor`, `width` attributes)
* `<td>`: cells (with `align`, `bgcolor`, `width` attributes)
* `<td>`: cells (with `align`, `bgcolor`, `width`, `rowspan`, `colspan` attributes)


## Known limitations
Expand Down
72 changes: 70 additions & 2 deletions docs/Tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Result:
* control table width & position
* control over text alignment in cells, globally or per row
* allow to embed images in cells
* merge cells across columns and rows

## Setting table & column widths

Expand Down Expand Up @@ -336,9 +337,9 @@ Result:

![](table_with_gutter.jpg)

## Column span
## Column span and row span

Cells spanning multiple columns can be defined by passing a `colspan` argument to `.cell()`.
Cells spanning multiple columns or rows can be defined by passing a `colspan` or `rowspan` argument to `.cell()`.
Only the cells with data in them need to be defined. This means that the number of cells on each row can be different.

```python
Expand Down Expand Up @@ -366,6 +367,73 @@ result:

![](image-colspan.png)



```python
...
with pdf.table(text_align="CENTER") as table:
row = table.row()
row.cell("A1", colspan=2, rowspan=3)
row.cell("C1", colspan=2)

row = table.row()
row.cell("C2", colspan=2, rowspan=2)

row = table.row()
# all columns of this row are spanned by previous rows

row = table.row()
row.cell("A4", colspan=4)

row = table.row()
row.cell("A5", colspan=2)
row.cell("C5")
row.cell("D5")

row = table.row()
row.cell("A6")
row.cell("B6", colspan=2, rowspan=2)
row.cell("D6", rowspan=2)

row = table.row()
row.cell("A7")
...
```

result:

![](image-rowspan.png)

Alternatively, the spans can be defined using the placeholder elements `TableSpan.COL` and `TableSpan.ROW`.
These elements merge the current cell with the previous column or row respectively.

For example, the previous example table can be defined as follows:

```python
...
TABLE_DATA = [
["A", "B", "C", "D"],
["A1", TableSpan.COL, "C1", TableSpan.COL],
[TableSpan.ROW, TableSpan.ROW, "C2", TableSpan.COL],
[TableSpan.ROW, TableSpan.ROW, TableSpan.ROW, TableSpan.ROW],
["A4", TableSpan.COL, TableSpan.COL, TableSpan.COL],
["A5", TableSpan.COL, "C5", "D5"],
["A6", "B6", TableSpan.COL, "D6"],
["A7", TableSpan.ROW, TableSpan.ROW, TableSpan.ROW],
]

with pdf.table(TABLE_DATA, text_align="CENTER"):
pass
...
```

result:

![](image-rowspan.png)




## Table with multiple heading rows

The number of heading rows is defined by passing the `num_heading_rows` argument to `Table()`. The default value is `1`. To guarantee backwards compatibility with the `first_row_as_headings` argument, the following applies:
Expand Down
Binary file added docs/image-rowspan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions fpdf/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ def should_fill_cell(self, i, j):
raise NotImplementedError


class TableSpan(CoerciveEnum):
ROW = intern("ROW")
"Mark this cell as a continuation of the previous row"

COL = intern("COL")
"Mark this cell as a continuation of the previous column"


class RenderStyle(CoerciveEnum):
"Defines how to render shapes"

Expand Down
4 changes: 4 additions & 0 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4914,6 +4914,7 @@ def table(self, *args, **kwargs):
line_height (number): optional. Defines how much vertical space a line of text will occupy.
markdown (bool): optional, default to False. Enable markdown interpretation of cells textual content.
text_align (str, fpdf.enums.Align): optional, default to JUSTIFY. Control text alignment inside cells.
v_align (str, fpdf.enums.AlignV): optional, default to CENTER. Control vertical alignment of cells content.
width (number): optional. Sets the table width.
wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default),
"CHAR" for character based line wrapping.
Expand All @@ -4922,6 +4923,9 @@ def table(self, *args, **kwargs):
If padding for left or right ends up being non-zero then the respective c_margin is ignored.
outer_border_width (number): optional. The outer_border_width will trigger rendering of the outer
border of the table with the given width regardless of any other defined border styles.
num_heading_rows (number): optional. Sets the number of heading rows, default value is 1. If this value is not 1,
first_row_as_headings needs to be True if num_heading_rows>1 and False if num_heading_rows=0. For backwards compatibility,
first_row_as_headings is used in case num_heading_rows is 1.
"""
table = Table(self, *args, **kwargs)
yield table
Expand Down
26 changes: 18 additions & 8 deletions fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .errors import FPDFException
from .deprecation import get_stack_level
from .fonts import FontFace
from .table import Table, TableBordersLayout
from .table import Table

LOGGER = logging.getLogger(__name__)
BULLET_WIN1252 = "\x95" # BULLET character in Windows-1252 encoding
Expand Down Expand Up @@ -375,6 +375,7 @@ def handle_data(self, data):
self.td_th.get("bgcolor", self.tr.get("bgcolor", None))
)
colspan = int(self.td_th.get("colspan", "1"))
rowspan = int(self.td_th.get("rowspan", "1"))
emphasis = 0
if self.td_th.get("b"):
emphasis |= TextEmphasis.B
Expand All @@ -387,7 +388,9 @@ def handle_data(self, data):
style = FontFace(
emphasis=emphasis, fill_color=bgcolor, color=self.pdf.text_color
)
self.table_row.cell(text=data, align=align, style=style, colspan=colspan)
self.table_row.cell(
text=data, align=align, style=style, colspan=colspan, rowspan=rowspan
)
self.td_th["inserted"] = True
elif self.table is not None:
# ignore anything else than td inside a table
Expand Down Expand Up @@ -553,23 +556,30 @@ def handle_starttag(self, tag, attrs):
width = self.pdf.epw * int(width[:-1]) / 100
else:
width = int(width) / self.pdf.k
if "border" in attrs:
borders_layout = (
"ALL" if self.table_line_separators else "NO_HORIZONTAL_LINES"
)
else:
if "border" not in attrs: # default borders
borders_layout = (
"HORIZONTAL_LINES"
if self.table_line_separators
else "SINGLE_TOP_LINE"
)
elif int(attrs["border"]): # explicitly enabled borders
borders_layout = (
"ALL" if self.table_line_separators else "NO_HORIZONTAL_LINES"
)
else: # explicitly disabled borders
borders_layout = "NONE"
align = attrs.get("align", "center").upper()
padding = float(attrs["cellpadding"]) if "cellpadding" in attrs else None
spacing = float(attrs.get("cellspacing", 0))
self.table = Table(
self.pdf,
align=align,
borders_layout=borders_layout,
line_height=self.h * 1.30,
width=width,
padding=padding,
gutter_width=spacing,
gutter_height=spacing,
)
self._ln()
if tag == "tr":
Expand All @@ -590,7 +600,7 @@ def handle_starttag(self, tag, attrs):
# => we are in the 1st <tr>, and the 1st cell is a <td>
# => we do not treat the first row as a header
# pylint: disable=protected-access
self.table._borders_layout = TableBordersLayout.NONE
self.table._first_row_as_headings = False
self.table._num_heading_rows = 0
if "height" in attrs:
LOGGER.warning(
Expand Down
Loading
Loading