diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42636f96c..1904c64b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
## [2.8.2] - Not released yet
### Added
+* new optional parameter `border` for table cells [issue #1192](https://github.com/py-pdf/fpdf2/issues/1192) users can define specific borders (left, right, top, bottom) for individual cells
* [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): now parses `
` tags to set the [document title](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_title). By default, it is added as PDF metadata, but not rendered in the document body. However, this can be enabled by passing `render_title_tag=True` to `FPDF.write_html()`.
* support for LZWDecode compression [issue #1271](https://github.com/py-pdf/fpdf2/issues/1271)
### Fixed
diff --git a/docs/Tables.md b/docs/Tables.md
index 464e42eae..d130d84b1 100644
--- a/docs/Tables.md
+++ b/docs/Tables.md
@@ -38,6 +38,7 @@ Result:
* control over borders: color, width & where they are drawn
* handle splitting a table over page breaks, with headings repeated
* control over cell background color
+* control over cell borders
* control table width & position
* control over text alignment in cells, globally or per row
* allow to embed images in cells
@@ -277,6 +278,68 @@ Result:
All the possible layout values are described
there: [`TableBordersLayout`](https://py-pdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.TableBordersLayout).
+## Set cell borders
+
+_New in [:octicons-tag-24: 2.8.2](https://github.com/py-pdf/fpdf2/blob/master/CHANGELOG.md)_
+
+```python
+from fpdf import FPDF
+
+pdf = FPDF()
+pdf.add_page()
+pdf.set_font("Times", size=16)
+with pdf.table() as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="LEFT")
+pdf.output('table.pdf')
+```
+
+Result:
+
+![](table_with_cell_border_left.jpg)
+
+```python
+from fpdf import FPDF
+
+pdf = FPDF()
+pdf.add_page()
+pdf.set_font("Times", size=16)
+with pdf.table() as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="TOP")
+pdf.output('table.pdf')
+```
+
+Result:
+
+![](table_with_cell_border_top.jpg)
+
+```python
+from fpdf import FPDF
+from fpdf.enums import CellBordersLayout
+
+pdf = FPDF()
+pdf.add_page()
+pdf.set_font("Times", size=16)
+with pdf.table() as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=CellBordersLayout.TOP | CellBordersLayout.LEFT)
+pdf.output('table.pdf')
+```
+
+Result:
+
+![](table_with_cell_border_left_top.jpg)
+
+All the possible borders values are described there: [`CellBordersLayout`](https://py-pdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.CellBordersLayout).
+
+
## Insert images
```python
diff --git a/docs/table_with_cell_border_left.jpg b/docs/table_with_cell_border_left.jpg
new file mode 100644
index 000000000..97f65c728
Binary files /dev/null and b/docs/table_with_cell_border_left.jpg differ
diff --git a/docs/table_with_cell_border_left_top.jpg b/docs/table_with_cell_border_left_top.jpg
new file mode 100644
index 000000000..e130fbff9
Binary files /dev/null and b/docs/table_with_cell_border_left_top.jpg differ
diff --git a/docs/table_with_cell_border_top.jpg b/docs/table_with_cell_border_top.jpg
new file mode 100644
index 000000000..a71f5e231
Binary files /dev/null and b/docs/table_with_cell_border_top.jpg differ
diff --git a/fpdf/enums.py b/fpdf/enums.py
index 22cb84cb5..d0188b951 100644
--- a/fpdf/enums.py
+++ b/fpdf/enums.py
@@ -310,6 +310,74 @@ class TableBordersLayout(CoerciveEnum):
"Draw only the top horizontal border, below the headings"
+class CellBordersLayout(CoerciveIntFlag):
+ """Defines how to render cell borders in table
+
+ The integer value of `border` determines which borders are applied. Below are some common examples:
+
+ - border=1 (LEFT): Only the left border is enabled.
+ - border=3 (LEFT | RIGHT): Both the left and right borders are enabled.
+ - border=5 (LEFT | TOP): The left and top borders are enabled.
+ - border=12 (TOP | BOTTOM): The top and bottom borders are enabled.
+ - border=15 (ALL): All borders (left, right, top, bottom) are enabled.
+ - border=16 (INHERIT): Inherit the border settings from the parent element.
+
+ Using `border=3` will combine LEFT and RIGHT borders, as it represents the
+ bitwise OR of `LEFT (1)` and `RIGHT (2)`.
+ """
+
+ NONE = 0
+ "Draw no border on any side of cell"
+
+ LEFT = 1
+ "Draw border on the left side of the cell"
+
+ RIGHT = 2
+ "Draw border on the right side of the cell"
+
+ TOP = 4
+ "Draw border on the top side of the cell"
+
+ BOTTOM = 8
+ "Draw border on the bottom side of the cell"
+
+ ALL = LEFT | RIGHT | TOP | BOTTOM
+ "Draw border on all side of the cell"
+
+ INHERIT = 16
+ "Inherits the border layout from the table borders layout"
+
+ @classmethod
+ def coerce(cls, value):
+ if isinstance(value, int) and value > 16:
+ raise ValueError("INHERIT cannot be combined with other values")
+ return super().coerce(value)
+
+ def __and__(self, value):
+ value = super().__and__(value)
+ if value > 16:
+ raise ValueError("INHERIT cannot be combined with other values")
+ return value
+
+ def __or__(self, value):
+ value = super().__or__(value)
+ if value > 16:
+ raise ValueError("INHERIT cannot be combined with other values")
+ return value
+
+ def __str__(self):
+ border_str = []
+ if self & CellBordersLayout.LEFT:
+ border_str.append("L")
+ if self & CellBordersLayout.RIGHT:
+ border_str.append("R")
+ if self & CellBordersLayout.TOP:
+ border_str.append("T")
+ if self & CellBordersLayout.BOTTOM:
+ border_str.append("B")
+ return "".join(border_str) if border_str else "NONE"
+
+
class TableCellFillMode(CoerciveEnum):
"Defines which table cells to fill"
diff --git a/fpdf/table.py b/fpdf/table.py
index 3d2528810..8d3a0902f 100644
--- a/fpdf/table.py
+++ b/fpdf/table.py
@@ -11,6 +11,7 @@
WrapMode,
VAlign,
TableSpan,
+ CellBordersLayout,
)
from .errors import FPDFException
from .fonts import CORE_FONTS, FontFace
@@ -251,6 +252,7 @@ def render(self):
self._fpdf.l_margin = prev_l_margin
self._fpdf.x = self._fpdf.l_margin
+ # pylint: disable=too-many-return-statements
def get_cell_border(self, i, j, cell):
"""
Defines which cell borders should be drawn.
@@ -258,6 +260,10 @@ def get_cell_border(self, i, j, cell):
to be passed to `fpdf.FPDF.multi_cell()`.
Can be overriden to customize this logic
"""
+
+ if cell.border != CellBordersLayout.INHERIT:
+ return str(cell.border)
+
if self._borders_layout == TableBordersLayout.ALL:
return 1
if self._borders_layout == TableBordersLayout.NONE:
@@ -770,6 +776,7 @@ def cell(
rowspan=1,
padding=None,
link=None,
+ border=CellBordersLayout.INHERIT,
):
"""
Adds a cell to the row.
@@ -788,6 +795,7 @@ def cell(
rowspan (int): optional number of rows this cell should span.
padding (tuple): optional padding (left, top, right, bottom) for the cell.
link (str, int): optional link, either an URL or an integer returned by `FPDF.add_link`, defining an internal link to a page
+ border (fpdf.enums.CellBordersLayout): optional cell borders, defaults to `CellBordersLayout.INHERIT`
"""
if text and img:
@@ -819,6 +827,7 @@ def cell(
rowspan,
padding,
link,
+ CellBordersLayout.coerce(border),
)
self.cells.append(cell)
return cell
@@ -838,6 +847,7 @@ class Cell:
"rowspan",
"padding",
"link",
+ "border",
)
text: str
align: Optional[Union[str, Align]]
@@ -849,6 +859,7 @@ class Cell:
rowspan: int
padding: Optional[Union[int, tuple, type(None)]]
link: Optional[Union[str, int]]
+ border: Optional[CellBordersLayout]
def write(self, text, align=None):
raise NotImplementedError("Not implemented yet")
diff --git a/test/table/test_table.py b/test/table/test_table.py
index 3e13f1880..257aedfd6 100644
--- a/test/table/test_table.py
+++ b/test/table/test_table.py
@@ -829,3 +829,213 @@ def test_table_with_very_long_text():
str(error.value)
== "The row with index 0 is too high and cannot be rendered on a single page"
)
+
+
+def test_table_cell_border_none(tmp_path):
+ pdf = FPDF()
+ pdf.add_page()
+ pdf.set_font("Times", size=20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="None")
+ assert_pdf_equal(pdf, HERE / "test_table_cell_border_none.pdf", tmp_path)
+
+
+def test_table_cell_different_borders(tmp_path):
+ pdf = FPDF()
+ pdf.set_font("Times", size=20)
+ pdf.add_page()
+ pdf.cell(
+ 0,
+ 10,
+ "border = left",
+ )
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="left")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = right")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="right")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = top")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="top")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = bottom")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="bottom")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = [left, right]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=3)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[left, top]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=5)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[left, bottom]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=9)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[right, top]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=6)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[right, bottom]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=10)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[top, bottom]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=12)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[left, right, top]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=7)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[left, right, bottom]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=11)
+ pdf.add_page()
+ pdf.cell(0, 10, "border =[left, right, top, bottom]")
+ pdf.ln(20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border=15)
+ assert_pdf_equal(pdf, HERE / "test_table_cell_different_borders.pdf", tmp_path)
+
+
+def test_table_cell_border_all(tmp_path):
+ pdf = FPDF()
+ pdf.add_page()
+ pdf.set_font("Times", size=20)
+ with pdf.table(gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+
+ for datum in data_row:
+ print(datum, end=" ")
+ row.cell(datum, border="all")
+ assert_pdf_equal(pdf, HERE / "test_table_cell_border_all.pdf", tmp_path)
+
+
+def test_table_cell_border_inherit(tmp_path):
+ pdf = FPDF()
+ pdf.add_page()
+ pdf.set_font("Times", size=20)
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'none'")
+ pdf.ln(20)
+ with pdf.table(borders_layout="none", gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'all'")
+ pdf.ln(20)
+ with pdf.table(borders_layout="all", gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+
+ for datum in data_row:
+ print(datum, end=" ")
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'horizontal_lines'")
+ pdf.ln(20)
+ with pdf.table(
+ borders_layout="horizontal_lines", gutter_height=3, gutter_width=3
+ ) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'internal' ")
+ pdf.ln(20)
+ with pdf.table(borders_layout="internal", gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'minimal'")
+ pdf.ln(20)
+ with pdf.table(borders_layout="minimal", gutter_height=3, gutter_width=3) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'no_horizontal_lines'")
+ pdf.ln(20)
+ with pdf.table(
+ borders_layout="no_horizontal_lines", gutter_height=3, gutter_width=3
+ ) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ pdf.add_page()
+ pdf.cell(0, 10, "border = 'inherit' and borders_layout = 'single_top_line'")
+ pdf.ln(20)
+ with pdf.table(
+ borders_layout="single_top_line", gutter_height=3, gutter_width=3
+ ) as table:
+ for data_row in TABLE_DATA:
+ row = table.row()
+ for datum in data_row:
+ row.cell(datum, border="inherit")
+ assert_pdf_equal(pdf, HERE / "test_table_cell_border_inherit.pdf", tmp_path)
diff --git a/test/table/test_table_cell_border_all.pdf b/test/table/test_table_cell_border_all.pdf
new file mode 100644
index 000000000..f830e4bbc
Binary files /dev/null and b/test/table/test_table_cell_border_all.pdf differ
diff --git a/test/table/test_table_cell_border_inherit.pdf b/test/table/test_table_cell_border_inherit.pdf
new file mode 100644
index 000000000..846a2c553
Binary files /dev/null and b/test/table/test_table_cell_border_inherit.pdf differ
diff --git a/test/table/test_table_cell_border_none.pdf b/test/table/test_table_cell_border_none.pdf
new file mode 100644
index 000000000..ca4e752ed
Binary files /dev/null and b/test/table/test_table_cell_border_none.pdf differ
diff --git a/test/table/test_table_cell_different_borders.pdf b/test/table/test_table_cell_different_borders.pdf
new file mode 100644
index 000000000..98062dab4
Binary files /dev/null and b/test/table/test_table_cell_different_borders.pdf differ