+ if width or height:
+ LOGGER.warning(
+ 'Ignoring unsupported "width" / "height" set on element'
+ )
+ if self.align:
+ LOGGER.warning("Ignoring unsupported alignment")
+ self.table_row.cell(img=attrs["src"], img_fill_width=True)
+ self.td_th["rendered"] = True
+ return
if self.pdf.y + height > self.pdf.page_break_trigger:
self.pdf.add_page(same=True)
- y = self.pdf.get_y()
- if self.table_col_index is not None:
- self._only_imgs_in_td = True
- # in a : its width must not exceed the cell width:
- td_width = self._td_width()
- if not width or width > td_width:
- if width: # Preserving image aspect ratio:
- height *= td_width / width
- width = td_width
- x = self._td_x()
- if self.align and self.align[0].upper() == "C":
- x += (td_width - width) / 2
- else:
- x = self.pdf.get_x()
- if self.align and self.align[0].upper() == "C":
- x = self.pdf.w / 2 - width / 2
+ x, y = self.pdf.get_x(), self.pdf.get_y()
+ if self.align and self.align[0].upper() == "C":
+ x = self.pdf.w / 2 - width / 2
LOGGER.debug(
'image "%s" x=%d y=%d width=%d height=%d',
attrs["src"],
@@ -636,20 +497,10 @@ def handle_starttag(self, tag, attrs):
width,
height,
)
- image_info = self.pdf.image(
+ info = self.pdf.image(
self.image_map(attrs["src"]), x, y, width, height, link=self.href
)
- width = image_info["rendered_width"]
- height = image_info["rendered_height"]
- self.pdf.set_x(x + width)
- if self.table_col_index is not None:
- # in a | : we grow the cell height according to the image height:
- if height > self.table_row_height:
- self.table_row_height = height
- else:
- self.pdf.set_y(y + height)
- if tag in ("b", "i", "u"):
- self.set_style(tag, True)
+ self.pdf.set_y(y + info.rendered_height)
if tag == "center":
self.align = "Center"
if tag == "toc":
@@ -708,7 +559,8 @@ def handle_endtag(self, tag):
if tag == "em":
tag = "i"
if tag in ("b", "i", "u"):
- self.set_style(tag, False)
+ if not self.td_th is not None:
+ self.set_style(tag, False)
self.follows_fmt_tag = True
if tag == "a":
self.href = ""
@@ -720,37 +572,22 @@ def handle_endtag(self, tag):
self.indent -= 1
self.bullet.pop()
if tag == "table":
- if not self.tfooter_out:
- self.output_table_footer()
+ with self.pdf.local_context(line_width=0.2):
+ self.table.render()
self.table = None
- self.th = False
- self.theader = None
- self.tfooter = None
self.pdf.ln(self.h)
- self.tr_index = None
- if tag == "thead":
- self.thead = None
- self.tr_index = None
- if tag == "tfoot":
- self.tfoot = None
- self.tr_index = None
- if tag == "tbody":
- self.tbody = None
- self.tr_index = None
if tag == "tr":
- if self.tfoot is None:
- self.pdf.ln(self.table_row_height)
- self.table_col_index = None
self.tr = None
+ self.table_row = None
if tag in ("td", "th"):
- if self.th:
- LOGGER.debug("revert style")
- self.set_style("b", False) # revert style
- elif self._only_imgs_in_td:
- self._insert_td()
- self.table_col_index += int(self.td.get("colspan", "1"))
- self.td = None
- self.th = False
+ if "rendered" not in self.td_th:
+ # handle_data() was not called => we call it to produce an empty cell:
+ bgcolor = color_as_decimal(
+ self.td_th.get("bgcolor", self.tr.get("bgcolor", None))
+ )
+ style = FontStyle(fill_color=bgcolor) if bgcolor else None
+ self.table_row.cell(text="", style=style)
+ self.td_th = None
if tag == "font":
# recover last font state
face, size, color = self.font_stack.pop()
diff --git a/fpdf/table.py b/fpdf/table.py
index 8676b7315..3ad25c150 100644
--- a/fpdf/table.py
+++ b/fpdf/table.py
@@ -1,6 +1,7 @@
from contextlib import contextmanager
+from dataclasses import dataclass
from numbers import Number
-from typing import List
+from typing import List, Union
from .enums import Align, TableBordersLayout
from .fonts import FontStyle
@@ -17,7 +18,7 @@ class Table:
def __init__(self, fpdf):
self._fpdf = fpdf
- self._rows = []
+ self.rows = []
self.align = "CENTER"
"""
Sets the table horizontal position relative to the page,
@@ -46,7 +47,7 @@ def __init__(self, fpdf):
def row(self):
"Adds a row to the table. Yields a `Row` object."
row = Row()
- self._rows.append(row)
+ self.rows.append(row)
yield row
def render(self):
@@ -67,22 +68,15 @@ def render(self):
self._fpdf.x = self._fpdf.l_margin
elif self._fpdf.x != self._fpdf.l_margin:
self._fpdf.l_margin = self._fpdf.x
- for i in range(len(self._rows)):
+ for i in range(len(self.rows)):
with self._fpdf.offset_rendering() as test:
- self._render_table_row_styled(i)
+ self._render_table_row(i)
if test.page_break_triggered:
# pylint: disable=protected-access
self._fpdf._perform_page_break()
if self.first_row_as_headings: # repeat headings on top:
- self._render_table_row_styled(0)
- if self.cell_fill_color:
- prev_fill_color = self._fpdf.fill_color
- self._fpdf.set_fill_color(self.cell_fill_color)
- else:
- prev_fill_color = None
- self._render_table_row_styled(i)
- if prev_fill_color:
- self._fpdf.set_fill_color(prev_fill_color)
+ self._render_table_row(0)
+ self._render_table_row(i)
self._fpdf.l_margin = prev_l_margin
self._fpdf.x = self._fpdf.l_margin
@@ -97,8 +91,8 @@ def get_cell_border(self, i, j):
return 1
if self.borders_layout == TableBordersLayout.NONE.value:
return 0
- columns_count = max(len(row.cells) for row in self._rows)
- rows_count = len(self._rows)
+ columns_count = max(row.cols_count for row in self.rows)
+ rows_count = len(self.rows)
border = list("LRTB")
if self.borders_layout == TableBordersLayout.INTERNAL.value:
if i == 0 and "T" in border:
@@ -110,39 +104,43 @@ def get_cell_border(self, i, j):
if j == columns_count - 1 and "R" in border:
border.remove("R")
if self.borders_layout == TableBordersLayout.MINIMAL.value:
+ if (i != 1 or rows_count == 1) and "T" in border:
+ border.remove("T")
if i != 0 and "B" in border:
border.remove("B")
- if rows_count > 1 and i != 1 and "T" in border:
- border.remove("T")
if j == 0 and "L" in border:
border.remove("L")
if j == columns_count - 1 and "R" in border:
border.remove("R")
if self.borders_layout == TableBordersLayout.NO_HORIZONTAL_LINES.value:
+ if i not in (0, 1) and "T" in border:
+ border.remove("T")
if i not in (0, rows_count - 1) and "B" in border:
border.remove("B")
- if rows_count > 1 and i not in (0, 1) and "T" in border:
+ if self.borders_layout == TableBordersLayout.HORIZONTAL_LINES.value:
+ if rows_count == 1:
+ return 0
+ border = list("TB")
+ if i == 0 and "T" in border:
border.remove("T")
+ if i == rows_count - 1 and "B" in border:
+ border.remove("B")
if self.borders_layout == TableBordersLayout.SINGLE_TOP_LINE.value:
+ if rows_count == 1:
+ return 0
border = list("TB")
+ if i != 1 and "T" in border:
+ border.remove("T")
if i != 0 and "B" in border:
border.remove("B")
- if rows_count > 1 and i != 1 and "T" in border:
- border.remove("T")
return "".join(border)
- def _render_table_row_styled(self, i):
- if i == 0 and self.first_row_as_headings:
- with self._fpdf.use_font_style(self.headings_style):
- self._render_table_row(i, fill=bool(self.headings_style.fill_color))
- else:
- self._render_table_row(i)
-
def _render_table_row(self, i, fill=False, **kwargs):
- row = self._rows[i]
+ row = self.rows[i]
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)):
+ j = 0
+ while j < len(row.cells):
cell_line_height = row_height / len(lines_heights_per_cell[j])
self._render_table_cell(
i,
@@ -152,6 +150,7 @@ def _render_table_row(self, i, fill=False, **kwargs):
fill=fill,
**kwargs,
)
+ j += row.cells[j].colspan
self._fpdf.ln(row_height)
# pylint: disable=inconsistent-return-statements
@@ -168,9 +167,9 @@ def _render_table_cell(
"""
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)
+ row = self.rows[i]
cell = row.cells[j]
+ col_width = self._get_col_width(i, j, cell.colspan)
lines_heights = []
if cell.img:
if lines_heights_only:
@@ -190,47 +189,62 @@ def _render_table_cell(
keep_aspect_ratio=True,
)
self._fpdf.set_xy(x, y)
- if not fill:
+ text_align = cell.align or self.text_align
+ if not isinstance(text_align, (Align, str)):
+ text_align = text_align[j]
+ style = cell.style
+ if not style and i == 0 and self.first_row_as_headings:
+ style = self.headings_style
+ if lines_heights_only and style:
+ style = style.replace(emphasis=None)
+ if style and style.fill_color:
+ fill = True
+ elif not fill:
fill = self.cell_fill_color and self.cell_fill_logic(i, j)
- text_align = (
- self.text_align
- if isinstance(self.text_align, (Align, str))
- else self.text_align[j]
- )
- lines = self._fpdf.multi_cell(
- w=col_width,
- h=row_height,
- txt=cell.text,
- max_line_height=cell_line_height,
- border=self.get_cell_border(i, j),
- align=text_align,
- 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 fill and self.cell_fill_color and not (style and style.fill_color):
+ style = (
+ style.replace(fill_color=self.cell_fill_color)
+ if style
+ else FontStyle(fill_color=self.cell_fill_color)
+ )
+ with self._fpdf.use_font_style(style):
+ lines = self._fpdf.multi_cell(
+ w=col_width,
+ h=row_height,
+ txt=cell.text,
+ max_line_height=cell_line_height,
+ border=self.get_cell_border(i, j),
+ align=text_align,
+ new_x="RIGHT",
+ new_y="TOP",
+ fill=fill,
+ split_only=lines_heights_only,
+ **kwargs,
+ )
+ if lines_heights_only and not cell.img:
+ lines_heights += (len(lines) or 1) * [self.line_height]
if lines_heights_only:
return lines_heights
- def _get_col_width(self, i, j):
+ def _get_col_width(self, i, j, colspan=1):
if not self.col_widths:
- cols_count = len(self._rows[i].cells)
- return self.width / cols_count
+ cols_count = self.rows[i].cols_count
+ return colspan * (self.width / cols_count)
if isinstance(self.col_widths, Number):
- return self.col_widths
+ return colspan * self.col_widths
if j >= len(self.col_widths):
raise ValueError(
f"Invalid .col_widths specified: missing width for table() column {j + 1} on row {i + 1}"
)
# pylint: disable=unsubscriptable-object
- col_ratio = self.col_widths[j] / sum(self.col_widths)
- return col_ratio * self.width
+ 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
+ return col_width
def _get_lines_heights_per_cell(self, i) -> List[List[int]]:
- row = self._rows[i]
+ row = self.rows[i]
lines_heights = []
for j in range(len(row.cells)):
lines_heights.append(
@@ -251,13 +265,21 @@ class Row:
def __init__(self):
self.cells = []
- def cell(self, text="", img=None, img_fill_width=False):
+ @property
+ def cols_count(self):
+ return sum(cell.colspan for cell in self.cells)
+
+ def cell(
+ self, text="", align=None, style=None, img=None, img_fill_width=False, colspan=1
+ ):
"""
Adds a cell to the row.
Args:
text (str): string content, can contain several lines.
In that case, the row height will grow proportionally.
+ align (str, fpdf.enums.Align): optional text alignment
+ style (fpdf.fonts.FontStyle): optional text style
img: optional. Either a string representing a file path to an image,
an URL to an image, an io.BytesIO, or a instance of `PIL.Image.Image`.
img_fill_width (bool): optional, defaults to False. Indicates to render the image
@@ -268,13 +290,15 @@ def cell(self, text="", img=None, img_fill_width=False):
"fpdf2 currently does not support inserting text with an image in the same table cell."
"Pull Requests are welcome to implement this 😊"
)
- self.cells.append(Cell(text, img, img_fill_width))
+ self.cells.append(Cell(text, align, style, img, img_fill_width, colspan))
+@dataclass
class Cell:
"Internal representation of a table cell"
-
- def __init__(self, text, img, img_fill_width):
- self.text = text
- self.img = img
- self.img_fill_width = img_fill_width
+ text: str
+ align: Union[str, Align]
+ style: FontStyle
+ img: str
+ img_fill_width: bool
+ colspan: int
diff --git a/test/html/test_customize_ul.pdf b/test/html/html_customize_ul.pdf
similarity index 100%
rename from test/html/test_customize_ul.pdf
rename to test/html/html_customize_ul.pdf
diff --git a/test/html/html_features.pdf b/test/html/html_features.pdf
index b31838a39..b0d3c5fba 100644
Binary files a/test/html/html_features.pdf and b/test/html/html_features.pdf differ
diff --git a/test/html/test_img_not_overlapping.pdf b/test/html/html_img_not_overlapping.pdf
similarity index 100%
rename from test/html/test_img_not_overlapping.pdf
rename to test/html/html_img_not_overlapping.pdf
diff --git a/test/html/html_table_line_separators.pdf b/test/html/html_table_line_separators.pdf
index 6dd04631d..37d328506 100644
Binary files a/test/html/html_table_line_separators.pdf and b/test/html/html_table_line_separators.pdf differ
diff --git a/test/html/html_table_line_separators_issue_137.pdf b/test/html/html_table_line_separators_issue_137.pdf
index d278ad893..c6d470aa4 100644
Binary files a/test/html/html_table_line_separators_issue_137.pdf and b/test/html/html_table_line_separators_issue_137.pdf differ
diff --git a/test/html/html_simple_table.pdf b/test/html/html_table_simple.pdf
similarity index 69%
rename from test/html/html_simple_table.pdf
rename to test/html/html_table_simple.pdf
index f5ef356c9..b5aa7478e 100644
Binary files a/test/html/html_simple_table.pdf and b/test/html/html_table_simple.pdf differ
diff --git a/test/html/bgcolor_in_table.pdf b/test/html/html_table_with_bgcolor.pdf
similarity index 58%
rename from test/html/bgcolor_in_table.pdf
rename to test/html/html_table_with_bgcolor.pdf
index 2c061fbea..c6050e45d 100644
Binary files a/test/html/bgcolor_in_table.pdf and b/test/html/html_table_with_bgcolor.pdf differ
diff --git a/test/html/html_table_with_border.pdf b/test/html/html_table_with_border.pdf
index 89a8d12f4..6c3f372f3 100644
Binary files a/test/html/html_table_with_border.pdf and b/test/html/html_table_with_border.pdf differ
diff --git a/test/html/html_table_with_empty_cell_contents.pdf b/test/html/html_table_with_empty_cell_contents.pdf
index efe61273c..ff8f4a293 100644
Binary files a/test/html/html_table_with_empty_cell_contents.pdf and b/test/html/html_table_with_empty_cell_contents.pdf differ
diff --git a/test/html/test_img_inside_html_table_centered_with_align.pdf b/test/html/html_table_with_img.pdf
similarity index 95%
rename from test/html/test_img_inside_html_table_centered_with_align.pdf
rename to test/html/html_table_with_img.pdf
index 22db3f56c..a10439485 100644
Binary files a/test/html/test_img_inside_html_table_centered_with_align.pdf and b/test/html/html_table_with_img.pdf differ
diff --git a/test/html/test_img_inside_html_table_without_explicit_dimensions.pdf b/test/html/html_table_with_img_without_explicit_dimensions.pdf
similarity index 95%
rename from test/html/test_img_inside_html_table_without_explicit_dimensions.pdf
rename to test/html/html_table_with_img_without_explicit_dimensions.pdf
index 78983577f..a10439485 100644
Binary files a/test/html/test_img_inside_html_table_without_explicit_dimensions.pdf and b/test/html/html_table_with_img_without_explicit_dimensions.pdf differ
diff --git a/test/html/test_img_inside_html_table_centered_with_caption.pdf b/test/html/html_table_with_imgs_captions_and_colspan.pdf
similarity index 97%
rename from test/html/test_img_inside_html_table_centered_with_caption.pdf
rename to test/html/html_table_with_imgs_captions_and_colspan.pdf
index 813841803..1b53a4e87 100644
Binary files a/test/html/test_img_inside_html_table_centered_with_caption.pdf and b/test/html/html_table_with_imgs_captions_and_colspan.pdf differ
diff --git a/test/html/test_html_whitespace_handling.pdf b/test/html/html_whitespace_handling.pdf
similarity index 100%
rename from test/html/test_html_whitespace_handling.pdf
rename to test/html/html_whitespace_handling.pdf
diff --git a/test/html/test_html.py b/test/html/test_html.py
index fb4fac153..062897613 100644
--- a/test/html/test_html.py
+++ b/test/html/test_html.py
@@ -26,7 +26,7 @@ def test_html_images(tmp_path):
f""
)
# Unable to text position of the image as write html moves to a new line after
- # adding the image but it can be seen in the produce test.pdf file.
+ # adding the image but it can be seen in the resulting html_images.pdf file.
assert round(pdf.get_x()) == 10
assert pdf.get_y() == pytest.approx(mm_after_image, abs=0.01)
@@ -83,8 +83,8 @@ def test_html_features(tmp_path):
" "
" | "
" "
- ' id | '
- ' name | '
+ " id | "
+ " name | "
"
"
" "
"
"
@@ -110,8 +110,8 @@ def test_html_features(tmp_path):
" "
"