Skip to content

Commit

Permalink
Implementing images in tables
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucas-C committed Feb 24, 2023
1 parent f60aa92 commit d21efd0
Show file tree
Hide file tree
Showing 17 changed files with 196 additions and 41 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/continuous-integration-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ jobs:
find . -name '*.pdf' | xargs -n 1 scripts/verapdf.py
scripts/verapdf.py # printing aggregated report
- name: Running tests ☑
env:
PYTHONMALLOCSTATS: 1
run: |
# Ensuring there is no `generate=True` left remaining in calls to assert_pdf_equal:
grep -IRF generate=True test/ && exit 1
Expand Down
39 changes: 38 additions & 1 deletion docs/Tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,11 @@ with pdf.table() as table:
```
Result:

![](table_with_internal__layout.jpg)
![](table_with_internal_layout.jpg)

```python
...
pdf.set_draw_color(100) # dark grey
with pdf.table() as table:
table.borders_layout = "MINIMAL"
...
Expand All @@ -120,6 +121,42 @@ Result:

![](table_with_minimal_layout.jpg)

## Insert images
```python
TABLE_DATA = (
("First name", "Last name", "Image", "City"),
("Jules", "Smith", "shirt.png", "San Juan"),
("Mary", "Ramos", "shirt.png", "Orlando"),
("Carlson", "Banks", "shirt.png", "Los Angeles"),
("Lucas", "Cimon", "shirt.png", "Angers"),
)
pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=16)
with pdf.table() as table:
for i, data_row in enumerate(TABLE_DATA):
with table.row() as row:
for j, datum in enumerate(data_row):
if j == 2 and i > 0:
row.cell(img=datum)
else:
row.cell(datum)
pdf.output('table_with_images.pdf')
```
Result:

![](table_with_images.jpg)

By default, images height & width are constrained by the row height (based on text content)
and the column width. To render bigger images, you can set the `table.line_height` parameter to increase the row height, or pass `img_fill_width=True` to `.cell()`:

```python
row.cell(img=datum, img_fill_width=True)
```
Result:

![](table_with_images_and_img_fill_width.jpg)

## Using write_html

Tables can also be defined in HTML using [`FPDF.write_html`](HTML.md).
Expand Down
Binary file added docs/table_with_images.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/table_with_images_and_img_fill_width.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified docs/table_with_internal_layout.jpg
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file modified docs/table_with_minimal_layout.jpg
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 9 additions & 8 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3660,6 +3660,14 @@ def image(
if self.oversized_images and info["usages"] == 1 and not dims:
info = self._downscale_image(name, img, info, w, h)

# Flowing mode
if y is None:
self._perform_page_break_if_need_be(h)
y = self.y
self.y += h
if x is None:
x = self.x

if keep_aspect_ratio:
ratio = info.width / info.height
if h * ratio < w:
Expand All @@ -3669,14 +3677,7 @@ def image(
y += (h - w / ratio) / 2
h = w / ratio

# Flowing mode
if y is None:
self._perform_page_break_if_need_be(h)
y = self.y
self.y += h
if x is None:
x = self.x
elif not isinstance(x, Number):
if not isinstance(x, Number):
if keep_aspect_ratio:
raise ValueError(
"FPDF.image(): 'keep_aspect_ratio' cannot be used with an enum value provided to `x`"
Expand Down
102 changes: 73 additions & 29 deletions fpdf/table.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import contextmanager
from numbers import Number
from typing import List

from .enums import TableBordersLayout
from .fonts import FontStyle
Expand All @@ -12,13 +13,13 @@ class Table:
def __init__(self, fpdf):
self._fpdf = fpdf
self._rows = []
self.borders_layout = TableBordersLayout.ALL
self.cell_fill_color = None
self.cell_fill_logic = lambda i, j: True
self.col_widths = None
self.first_row_as_headings = True
self.headings_style = DEFAULT_HEADINGS_STYLE
self.line_height = 2 * fpdf.font_size
self.borders_layout = TableBordersLayout.ALL
self.width = fpdf.epw

@contextmanager
Expand Down Expand Up @@ -81,36 +82,74 @@ def _render_table_row_styled(self, i):

def _render_table_row(self, i, fill=False, **kwargs):
row = self._rows[i]
lines_count_per_cell = self._get_lines_count_per_cell(i)
row_height = max(lines_count_per_cell) * self.line_height
lines_heights_per_cell = self._get_lines_heights_per_cell(i)
row_height = max(sum(lines_heights) for lines_heights in lines_heights_per_cell)
for j in range(len(row.cells)):
cell_line_height = row_height / lines_count_per_cell[j]
if fill:
cell_fill = True
else:
cell_fill = self.cell_fill_color and self.cell_fill_logic(i, j)
cell_line_height = row_height / len(lines_heights_per_cell[j])
self._render_table_cell(
i,
j,
h=row_height,
max_line_height=cell_line_height,
fill=cell_fill,
cell_line_height=cell_line_height,
row_height=row_height,
fill=fill,
**kwargs,
)
self._fpdf.ln(row_height)

def _render_table_cell(self, i, j, h, **kwargs):
# pylint: disable=inconsistent-return-statements
def _render_table_cell(
self,
i,
j,
cell_line_height,
row_height,
fill=False,
lines_heights_only=False,
**kwargs,
):
"""
If `lines_heights_only` is True, returns a list of lines (subcells) heights.
"""
row = self._rows[i]
col_width = self._get_col_width(i, j)
return self._fpdf.multi_cell(
cell = row.cells[j]
lines_heights = []
if cell.img:
if lines_heights_only:
info = self._fpdf.preload_image(cell.img)[2]
img_ratio = info.width / info.height
if cell.img_fill_width or row_height * img_ratio > col_width:
img_height = col_width / img_ratio
else:
img_height = row_height
lines_heights += [img_height]
else:
x, y = self._fpdf.x, self._fpdf.y
self._fpdf.image(
cell.img,
w=col_width,
h=0 if cell.img_fill_width else row_height,
keep_aspect_ratio=True,
)
self._fpdf.set_xy(x, y)
if not fill:
fill = self.cell_fill_color and self.cell_fill_logic(i, j)
lines = self._fpdf.multi_cell(
w=col_width,
h=h,
txt=row.cells[j],
h=row_height,
txt=cell.text or "",
max_line_height=cell_line_height,
border=self.get_cell_border(i, j),
new_x="RIGHT",
new_y="TOP",
fill=fill,
split_only=lines_heights_only,
**kwargs,
)
if lines_heights_only and cell.text:
lines_heights += len(lines) * [self.line_height]
if lines_heights_only:
return lines_heights

def _get_col_width(self, i, j):
if not self.col_widths:
Expand All @@ -126,27 +165,32 @@ def _get_col_width(self, i, j):
col_ratio = self.col_widths[j] / sum(self.col_widths)
return col_ratio * self.width

def _get_lines_count_per_cell(self, i):
def _get_lines_heights_per_cell(self, i) -> List[List[int]]:
row = self._rows[i]
lines_count = []
lines_heights = []
for j in range(len(row.cells)):
lines_count.append(
len(
self._render_table_cell(
i,
j,
h=self.line_height,
max_line_height=self.line_height,
split_only=True,
)
lines_heights.append(
self._render_table_cell(
i,
j,
cell_line_height=self.line_height,
row_height=self.line_height,
lines_heights_only=True,
)
)
return lines_count
return lines_heights


class Row:
def __init__(self):
self.cells = []

def cell(self, text):
self.cells.append(text)
def cell(self, text=None, img=None, img_fill_width=False):
self.cells.append(Cell(text, img, img_fill_width))


class Cell:
def __init__(self, text, img, img_fill_width):
self.text = text
self.img = img
self.img_fill_width = img_fill_width
Binary file added test/table/table_with_an_image.pdf
Binary file not shown.
Binary file not shown.
Binary file modified test/table/table_with_minimal_layout.pdf
Binary file not shown.
Binary file modified test/table/table_with_multiline_cells.pdf
Binary file not shown.
Binary file modified test/table/table_with_multiline_cells_and_fixed_row_height.pdf
Binary file not shown.
Binary file modified test/table/table_with_multiline_cells_and_split_over_3_pages.pdf
Binary file not shown.
Binary file modified test/table/table_with_multiline_cells_and_without_headings.pdf
Binary file not shown.
3 changes: 2 additions & 1 deletion test/table/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@


HERE = Path(__file__).resolve().parent
FONTS_DIR = HERE.parent / "fonts"

TABLE_DATA = (
("First name", "Last name", "Age", "City"),
Expand Down Expand Up @@ -237,6 +236,8 @@ def test_table_with_minimal_layout(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=16)
pdf.set_draw_color(100) # dark grey
pdf.set_line_width(1)
with pdf.table() as table:
table.borders_layout = "MINIMAL"
for data_row in TABLE_DATA:
Expand Down
74 changes: 74 additions & 0 deletions test/table/test_table_with_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from pathlib import Path

from fpdf import FPDF
from test.conftest import assert_pdf_equal, LOREM_IPSUM


HERE = Path(__file__).resolve().parent
IMG_DIR = HERE.parent / "image"

TABLE_DATA = (
("First name", "Last name", "Image", "City"),
(
"Jules",
"Smith",
IMG_DIR / "png_images/ba2b2b6e72ca0e4683bb640e2d5572f8.png",
"San Juan",
),
(
"Mary",
"Ramos",
IMG_DIR / "png_images/ac6343a98f8edabfcc6e536dd75aacb0.png",
"Orlando",
),
(
"Carlson",
"Banks",
IMG_DIR / "image_types/insert_images_insert_png.png",
"Los Angeles",
),
("Lucas", "Cimon", IMG_DIR / "image_types/circle.bmp", "Angers"),
)
MULTILINE_TABLE_DATA = (
("Extract", "Text length"),
(LOREM_IPSUM[:200], str(len(LOREM_IPSUM[:200]))),
(LOREM_IPSUM[200:400], str(len(LOREM_IPSUM[200:400]))),
(LOREM_IPSUM[400:600], str(len(LOREM_IPSUM[400:600]))),
(LOREM_IPSUM[600:800], str(len(LOREM_IPSUM[600:800]))),
(LOREM_IPSUM[800:1000], str(len(LOREM_IPSUM[800:1000]))),
(LOREM_IPSUM[1000:1200], str(len(LOREM_IPSUM[1000:1200]))),
)


def test_table_with_an_image(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=16)
with pdf.table() as table:
for i, data_row in enumerate(TABLE_DATA):
with table.row() as row:
for j, datum in enumerate(data_row):
if j == 2 and i > 0:
row.cell(img=datum)
else:
row.cell(datum)
assert_pdf_equal(pdf, HERE / "table_with_an_image.pdf", tmp_path)


def test_table_with_an_image_and_img_fill_width(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.set_font("Times", size=16)
with pdf.table() as table:
for i, data_row in enumerate(TABLE_DATA):
with table.row() as row:
for j, datum in enumerate(data_row):
if j == 2 and i > 0:
row.cell(img=datum, img_fill_width=True)
else:
row.cell(datum)
assert_pdf_equal(
pdf,
HERE / "table_with_an_image_and_img_fill_width.pdf",
tmp_path,
)

0 comments on commit d21efd0

Please sign in to comment.