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