Skip to content

Commit

Permalink
New feature: table gutter (#787)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C authored May 31, 2023
1 parent 4afcf4c commit 4827901
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions docs/Tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:

Expand Down Expand Up @@ -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.

Expand Down
Binary file added docs/table_with_gutter.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
30 changes: 23 additions & 7 deletions fpdf/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -13,7 +13,7 @@

@dataclass(frozen=True)
class RowLayoutInfo:
height: int
height: float
triggers_page_jump: bool


Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions test/encryption/test_encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -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--."
)
Expand Down
Binary file added test/table/table_with_gutter.pdf
Binary file not shown.
31 changes: 31 additions & 0 deletions test/table/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
1 change: 0 additions & 1 deletion test/table/test_table_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4827901

Please sign in to comment.