From 48279015987b859c0d563c958a9f4d0d822b10ba Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Wed, 31 May 2023 19:25:51 +0200 Subject: [PATCH] New feature: table gutter (#787) --- CHANGELOG.md | 1 + docs/Tables.md | 14 +++++++++---- docs/table_with_gutter.jpg | Bin 0 -> 53202 bytes fpdf/fpdf.py | 24 +++++++++++++-------- fpdf/table.py | 30 ++++++++++++++++++++------- test/encryption/test_encryption.py | 1 + test/table/table_with_gutter.pdf | Bin 0 -> 1956 bytes test/table/test_table.py | 31 ++++++++++++++++++++++++++++ test/table/test_table_extraction.py | 1 - 9 files changed, 81 insertions(+), 21 deletions(-) create mode 100644 docs/table_with_gutter.jpg create mode 100644 test/table/table_with_gutter.pdf diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f3a46ce..168fc762d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.7.5] - Not released yet ### Added - [`FPDF.mirror()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.mirror) - New method: [documentation page](https://pyfpdf.github.io/fpdf2/Transformations.html) - Contributed by @sebastiantia +- [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): new optional parameters `gutter_height`, `gutter_width` and `wrapmode` ## [2.7.4] - 2023-04-28 ### Added diff --git a/docs/Tables.md b/docs/Tables.md index 755a3107d..404e1b84c 100644 --- a/docs/Tables.md +++ b/docs/Tables.md @@ -185,7 +185,6 @@ Result: ![](table_with_images_and_img_fill_width.jpg) ## Syntactic sugar - To simplify `table()` usage, shorter, alternative usage forms are allowed. This sample code: @@ -212,12 +211,20 @@ with pdf.table(TABLE_DATA): pass ``` -## Table from pandas DataFrame +## Gutter +Spacing can be introduced between rows and/or columns: +```python +with pdf.table(TABLE_DATA, gutter_height=3, gutter_width=3): + pass +``` +Result: + +![](table_with_gutter.jpg) +## Table from pandas DataFrame _cf._ [Maths documentation page](Maths.md#using-pandas) ## Using write_html - Tables can also be defined in HTML using [`FPDF.write_html`](HTML.md). With the same `data` as above, and column widths defined as percent of the effective width: @@ -250,7 +257,6 @@ pdf.output('table_html.pdf') Note that `write_html` has [some limitations, notably regarding multi-lines cells](HTML.md#supported-html-features). ## "Parsabilty" of the tables generated - The PDF file format is not designed to embed structured tables. Hence, it can be tricky to extract tables data from PDF documents. diff --git a/docs/table_with_gutter.jpg b/docs/table_with_gutter.jpg new file mode 100644 index 0000000000000000000000000000000000000000..629915a0703f81e09b6aa19d805966d86a495dc8 GIT binary patch literal 53202 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 z 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 K@Gyaf|2F|JZBe}d literal 0 HcmV?d00001 diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 5b436fbd7..f04a657b5 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -3564,10 +3564,10 @@ def multi_cell( print_sh=print_sh, wrapmode=wrapmode, ) - text_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) - while (text_line) is not None: - text_lines.append(text_line) - text_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) + txt_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) + while (txt_line) is not None: + text_lines.append(txt_line) + txt_line = multi_line_break.get_line_of_given_width(maximum_allowed_width) if not text_lines: # ensure we display at least one cell - cf. issue #349 text_lines = [ @@ -3647,7 +3647,8 @@ def multi_cell( # pretend we started at the top of the text block on the new page. # cf. test_multi_cell_table_with_automatic_page_break prev_y = self.y - if text_line.trailing_nl and new_y in (YPos.LAST, YPos.NEXT): + # pylint: disable=undefined-loop-variable + if text_line and text_line.trailing_nl and new_y in (YPos.LAST, YPos.NEXT): # The line renderer can't handle trailing newlines in the text. self.ln() @@ -3728,15 +3729,15 @@ def write( ) # first line from current x position to right margin first_width = self.w - self.x - self.r_margin - text_line = multi_line_break.get_line_of_given_width( + txt_line = multi_line_break.get_line_of_given_width( first_width - 2 * self.c_margin, wordsplit=False ) # remaining lines fill between margins full_width = self.w - self.l_margin - self.r_margin fit_width = full_width - 2 * self.c_margin - while (text_line) is not None: - text_lines.append(text_line) - text_line = multi_line_break.get_line_of_given_width(fit_width) + while txt_line is not None: + text_lines.append(txt_line) + txt_line = multi_line_break.get_line_of_given_width(fit_width) if not text_lines: return False @@ -3758,6 +3759,7 @@ def write( link=link, ) page_break_triggered = page_break_triggered or new_page + # pylint: disable=undefined-loop-variable if text_line.trailing_nl: # The line renderer can't handle trailing newlines in the text. self.ln() @@ -4802,12 +4804,16 @@ def table(self, *args, **kwargs): col_widths (int, tuple): optional. Sets column width. Can be a single number or a sequence of numbers first_row_as_headings (bool): optional, default to True. If False, the first row of the table is not styled differently from the others + gutter_height (float): optional vertical space between rows + gutter_width (float): optional horizontal space between columns headings_style (fpdf.fonts.FontFace): optional, default to bold. Defines the visual style of the top headings row: size, color, emphasis... 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. width (number): optional. Sets the table width + wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default), + "CHAR" for character based line wrapping. """ table = Table(self, *args, **kwargs) yield table diff --git a/fpdf/table.py b/fpdf/table.py index e40cefbf1..6d6d91f4c 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -2,7 +2,7 @@ from numbers import Number from typing import Optional, Union -from .enums import Align, TableBordersLayout, TableCellFillMode +from .enums import Align, TableBordersLayout, TableCellFillMode, WrapMode from .enums import MethodReturnValue from .errors import FPDFException from .fonts import FontFace @@ -13,7 +13,7 @@ @dataclass(frozen=True) class RowLayoutInfo: - height: int + height: float triggers_page_jump: bool @@ -34,11 +34,14 @@ def __init__( cell_fill_mode=TableCellFillMode.NONE, col_widths=None, first_row_as_headings=True, + gutter_height=0, + gutter_width=0, headings_style=DEFAULT_HEADINGS_STYLE, line_height=None, markdown=False, text_align="JUSTIFY", width=None, + wrapmode=WrapMode.WORD, ): """ Args: @@ -47,18 +50,22 @@ def __init__( align (str, fpdf.enums.Align): optional, default to CENTER. Sets the table horizontal position relative to the page, when it's not using the full page width borders_layout (str, fpdf.enums.TableBordersLayout): optional, default to ALL. Control what cell borders are drawn - cell_fill_color (int, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): optional. + cell_fill_color (float, tuple, fpdf.drawing.DeviceGray, fpdf.drawing.DeviceRGB): optional. Defines the cells background color cell_fill_mode (str, fpdf.enums.TableCellFillMode): optional. Defines which cells are filled with color in the background - col_widths (int, tuple): optional. Sets column width. Can be a single number or a sequence of numbers + col_widths (float, tuple): optional. Sets column width. Can be a single number or a sequence of numbers first_row_as_headings (bool): optional, default to True. If False, the first row of the table is not styled differently from the others + gutter_height (float): optional vertical space between rows + gutter_width (float): optional horizontal space between columns headings_style (fpdf.fonts.FontFace): optional, default to bold. Defines the visual style of the top headings row: size, color, emphasis... 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. width (number): optional. Sets the table width + wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default), + "CHAR" for character based line wrapping. """ self._fpdf = fpdf self._align = align @@ -67,11 +74,14 @@ def __init__( self._cell_fill_mode = TableCellFillMode.coerce(cell_fill_mode) self._col_widths = col_widths self._first_row_as_headings = first_row_as_headings + self._gutter_height = gutter_height + self._gutter_width = gutter_width self._headings_style = headings_style self._line_height = 2 * fpdf.font_size if line_height is None else line_height self._markdown = markdown self._text_align = text_align self._width = fpdf.epw if width is None else width + self._wrapmode = wrapmode self.rows = [] for row in rows: self.row(row) @@ -131,6 +141,8 @@ def render(self): self._fpdf._perform_page_break() if self._first_row_as_headings: # repeat headings on top: self._render_table_row(0) + elif i and self._gutter_height: + self._fpdf.y += self._gutter_height self._render_table_row(i, row_layout_info) # Restoring altered FPDF settings: self._fpdf.l_margin = prev_l_margin @@ -218,6 +230,8 @@ def _render_table_cell( row = self.rows[i] cell = row.cells[j] col_width = self._get_col_width(i, j, cell.colspan) + if j and self._gutter_width: + self._fpdf.x += self._gutter_width img_height = 0 if cell.img: x, y = self._fpdf.x, self._fpdf.y @@ -267,14 +281,16 @@ def _render_table_cell( fill=fill, markdown=self._markdown, output=MethodReturnValue.PAGE_BREAK | MethodReturnValue.HEIGHT, + wrapmode=self._wrapmode, **kwargs, ) return page_break, max(img_height, cell_height) def _get_col_width(self, i, j, colspan=1): + cols_count = self.rows[i].cols_count + width = self._width - (cols_count - 1) * self._gutter_width if not self._col_widths: - cols_count = self.rows[i].cols_count - return colspan * (self._width / cols_count) + return colspan * (width / cols_count) if isinstance(self._col_widths, Number): return colspan * self._col_widths if j >= len(self._col_widths): @@ -284,7 +300,7 @@ def _get_col_width(self, i, j, colspan=1): col_width = 0 for k in range(j, j + colspan): col_ratio = self._col_widths[k] / sum(self._col_widths) - col_width += col_ratio * self._width + col_width += col_ratio * width return col_width def _get_row_layout_info(self, i): diff --git a/test/encryption/test_encryption.py b/test/encryption/test_encryption.py index b39a18dfc..3b8089b26 100644 --- a/test/encryption/test_encryption.py +++ b/test/encryption/test_encryption.py @@ -166,6 +166,7 @@ def test_encrypt_font(tmp_path): ) pdf.set_font("Quicksand", size=32) text = ( + # pylint: disable=implicit-str-concat "Lorem ipsum dolor, **consectetur adipiscing** elit," " eiusmod __tempor incididunt__ ut labore et dolore --magna aliqua--." ) diff --git a/test/table/table_with_gutter.pdf b/test/table/table_with_gutter.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0ce031fb10fb0d5db0c17d61b10f5c28bebed724 GIT binary patch literal 1956 zcmbuAeNYr-9LJMLxJ|{vd_f%+jaR|Dy}Mm*4N>mS4UmI_Ls5w2fQ38Q+tu5}fJpNN z)X>D2&<^g>RFLpg^b*jtA+r&JOnDR2&{UX05k)|Srgjg=D`)DDo|&87`##U_`+J$$ z&*QF?V}8(F5deV#kV;Gg0s;WOjLIMx5CZs%waGLXCt|mv;sL$_Pu8lXR3;cFU=e$lgQMJnxIl`(r_g{ubyOZlA%HIaEwx|Xa9~m zMBy}*p-;kT(BIaQOK#)Gg^v1RT7tp#Y^{Wv!R2@om5c-YFq}+fG$0D0TolddakUP} zEH>|1Of?H8<_-)2e09=2$qz|wOPRm$=t35YsuQxBtrY_T`=Rzn#pg3^ijGLrtD$tSdn@v>ZI)I#hOj?{roC zK*h;;Ss$;!DD~QMeVJWKTut19y=w@f-Oy~#`k*>+hHE`}?dP*l`LPQ_RO{_Cn)#Kx z(h#$U0lF%9TP*L8pD)6b8VPBD8+D2EQuRw7nmsaf9s5FR}fK2ZDR zDW@Do>cHA+12XQEbGRuTDfLKfZG(E6FItVZu|~SLVm?0Bk@$PhSE!Wel8nl&!LWVW`YW## zSqo7pj&J;?W7GMRYd5;_g=d}`yQDl#Yqe?%^GO#PU!5{$i8bXudrQ|@oiw{IVpGGa zugirWc~|EJEAHe#!|g0lfCuWko>~T%rdq=GySTtRdM^~$O>i^^g~RK%3QN}<5!PV` zUF7qt!^K!3W?UU5k?A+k8KP_@7mI$3x++-)&@Nvp7qxS%nsa**hI*ciRG z3!2?paF`c5HOK0J!8K7p>69)%lV_cif=e32+n{P2vmd@#=CLgFmiWoMLsNI8IX2n5 zj~kv&ks-l5w?QR!dV5u1{IjC`RRMU3y-({bWyO2QxIb>kl!tv^R?w*TxmvY$kmu9* z$Y^dU?Dj3{ztX--=-KXqvOlDA`kKIg9%Msdjx>Y6YDW@I7EE{G% zJ0yCkcMO*E{-;h8pL9U3RbTa(*G=&+h<|8G4PUrog^laKSTAqes>DWnvU{Qom>{Q8SgTmu0Cd<+7)-dIv%LXayVilq}@si%@uI0MG< zS=zIG;+br>AvzX$(vhcZ>G{&5aQSmuvvj9#rJaJ}_1RB5wuwhew1MKK_Eow36WAyX)j8xQv`2jc=D zG)RaDMHngYAOt~$h(O{GA#9x}CJy}X6V}Poj9SlFN5@9OfV=ym2n_fWxr5WG literal 0 HcmV?d00001 diff --git a/test/table/test_table.py b/test/table/test_table.py index b3bb35fcb..66186e096 100644 --- a/test/table/test_table.py +++ b/test/table/test_table.py @@ -41,6 +41,23 @@ def test_table_simple(tmp_path): assert_pdf_equal(pdf, HERE / "table_simple.pdf", tmp_path) +def test_table_with_no_row(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Times", size=16) + with pdf.table(): + pass + + +def test_table_with_no_column(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Times", size=16) + with pdf.table() as table: + for _ in TABLE_DATA: + table.row() + + def test_table_with_syntactic_sugar(tmp_path): pdf = FPDF() pdf.add_page() @@ -379,3 +396,17 @@ def test_table_with_cell_overflow(tmp_path): row.cell("B2") row.cell("B3") assert_pdf_equal(pdf, HERE / "table_with_cell_overflow.pdf", tmp_path) + + +def test_table_with_gutter(tmp_path): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Times", size=16) + with pdf.table(TABLE_DATA, gutter_height=3, gutter_width=3): + pass + pdf.ln(10) + with pdf.table( + TABLE_DATA, borders_layout="SINGLE_TOP_LINE", gutter_height=3, gutter_width=3 + ): + pass + assert_pdf_equal(pdf, HERE / "table_with_gutter.pdf", tmp_path) diff --git a/test/table/test_table_extraction.py b/test/table/test_table_extraction.py index d37586dc1..ca9e5c0e6 100644 --- a/test/table/test_table_extraction.py +++ b/test/table/test_table_extraction.py @@ -11,7 +11,6 @@ import pytest import tabula -# pylint: disable=import-error,no-name-in-module from test.table.test_table import TABLE_DATA HERE = Path(__file__).resolve().parent