From 843a0919b09db3d280c86a7b6341fb0a309d4038 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Fri, 24 Feb 2023 21:39:45 +0100 Subject: [PATCH] Using new Table rendering in write_html() --- .../continuous-integration-workflow.yml | 3 +- CHANGELOG.md | 3 + README.md | 2 +- docs/index.md | 2 +- fpdf/enums.py | 16 +- fpdf/fonts.py | 22 +- fpdf/fpdf.py | 3 + fpdf/html.py | 383 +++++------------- fpdf/table.py | 158 +++++--- ...customize_ul.pdf => html_customize_ul.pdf} | Bin test/html/html_features.pdf | Bin 5989 -> 6012 bytes ...pping.pdf => html_img_not_overlapping.pdf} | Bin test/html/html_table_line_separators.pdf | Bin 1231 -> 1262 bytes .../html_table_line_separators_issue_137.pdf | Bin 1223 -> 1257 bytes ...simple_table.pdf => html_table_simple.pdf} | Bin 1221 -> 1237 bytes ..._table.pdf => html_table_with_bgcolor.pdf} | Bin 1623 -> 1658 bytes test/html/html_table_with_border.pdf | Bin 1309 -> 1353 bytes .../html_table_with_empty_cell_contents.pdf | Bin 1280 -> 1306 bytes ...with_align.pdf => html_table_with_img.pdf} | Bin 14801 -> 14888 bytes ..._with_img_without_explicit_dimensions.pdf} | Bin 14788 -> 14888 bytes ..._table_with_imgs_captions_and_colspan.pdf} | Bin 22162 -> 22186 bytes ...dling.pdf => html_whitespace_handling.pdf} | Bin test/html/test_html.py | 239 +---------- test/html/test_html_table.py | 184 +++++++++ test/html/test_img_inside_html_table.pdf | Bin 14797 -> 0 bytes .../test_img_inside_html_table_centered.pdf | Bin 14796 -> 0 bytes test/table/table_align.pdf | Bin 2112 -> 2124 bytes test/table/table_simple.pdf | Bin 1640 -> 1646 bytes test/table/table_with_cell_fill.pdf | Bin 1707 -> 2250 bytes test/table/table_with_cell_fill2.pdf | Bin 1730 -> 0 bytes test/table/table_with_fixed_col_width.pdf | Bin 1640 -> 1647 bytes test/table/table_with_fixed_row_height.pdf | Bin 1624 -> 1630 bytes test/table/table_with_fixed_width.pdf | Bin 1636 -> 1642 bytes test/table/table_with_headings_styled.pdf | Bin 1694 -> 1708 bytes test/table/table_with_images.pdf | Bin 78789 -> 78792 bytes .../table_with_images_and_img_fill_width.pdf | Bin 78790 -> 78795 bytes test/table/table_with_internal_layout.pdf | Bin 1569 -> 1572 bytes test/table/table_with_minimal_layout.pdf | Bin 1493 -> 1498 bytes test/table/table_with_multiline_cells.pdf | Bin 3044 -> 3050 bytes ...h_multiline_cells_and_fixed_row_height.pdf | Bin 2969 -> 2974 bytes .../table_with_multiline_cells_and_images.pdf | Bin 79961 -> 79966 bytes ...multiline_cells_and_split_over_3_pages.pdf | Bin 5242 -> 5254 bytes .../table_with_single_top_line_layout.pdf | Bin 1350 -> 1360 bytes test/table/table_with_varying_col_widths.pdf | Bin 1637 -> 1643 bytes test/table/test_table.py | 10 +- 45 files changed, 429 insertions(+), 596 deletions(-) rename test/html/{test_customize_ul.pdf => html_customize_ul.pdf} (100%) rename test/html/{test_img_not_overlapping.pdf => html_img_not_overlapping.pdf} (100%) rename test/html/{html_simple_table.pdf => html_table_simple.pdf} (69%) rename test/html/{bgcolor_in_table.pdf => html_table_with_bgcolor.pdf} (58%) rename test/html/{test_img_inside_html_table_centered_with_align.pdf => html_table_with_img.pdf} (95%) rename test/html/{test_img_inside_html_table_without_explicit_dimensions.pdf => html_table_with_img_without_explicit_dimensions.pdf} (95%) rename test/html/{test_img_inside_html_table_centered_with_caption.pdf => html_table_with_imgs_captions_and_colspan.pdf} (97%) rename test/html/{test_html_whitespace_handling.pdf => html_whitespace_handling.pdf} (100%) create mode 100644 test/html/test_html_table.py delete mode 100644 test/html/test_img_inside_html_table.pdf delete mode 100644 test/html/test_img_inside_html_table_centered.pdf delete mode 100644 test/table/table_with_cell_fill2.pdf diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index b5453c0f2..1ce307422 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -74,8 +74,7 @@ jobs: sed -i "s/author:.*/author: v$(python setup.py -V 2>/dev/null)/" mkdocs.yml cp tutorial/notebook.ipynb docs/ mkdocs build - pdoc --html -o public/ fpdf --config "git_link_template='https://github -.com/PyFPDF/fpdf2/blob/{commit}/{path}#L{start_line}-L{end_line}'" + pdoc --html -o public/ fpdf --config "git_link_template='https://github.com/PyFPDF/fpdf2/blob/{commit}/{path}#L{start_line}-L{end_line}'" cd contributors/ && PYTHONUNBUFFERED=1 ./build_contributors_html_page.py PyFPDF/fpdf2 cp -t ../public/ contributors.html contributors-map-small.png - name: Deploy documentation 🚀 diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e0bd6e6..07d78bd70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,12 @@ This can also be enabled programmatically with `warnings.simplefilter('default', - unicode (non limited to ASCII) text can now be provided as metadata [#685](https://github.com/PyFPDF/fpdf2/issues/685) - all `TitleStyle` constructor parameters are now effectively optional ### Changed +* [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now uses the new [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/Tables.html) method to render `` tags. As a consequence, vertical space before `
` tags has sometimes been reduced. - vector images parsing is now more robust: `fpdf2` can now embed SVG files without `viewPort` or no `height` / `width` - bitonal images are now encoded using `CCITTFaxDecode`, reducing their size in the PDF document - thanks to @eroux - when possible, JPG and group4 encoded TIFFs are now embedded directly without recompression - thanks to @eroux +### Removed +* [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) now uses the new [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/Tables.html) method to render `
` tags. As a consequence, it does not support the `height` attribute defined on `` tags. ## [2.6.1] - 2023-01-13 ### Added diff --git a/README.md b/README.md index 2632ef744..141629424 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ pip install git+https://github.com/PyFPDF/fpdf2.git@master * Embedding images, including transparency and alpha channel * Arbitrary path drawing and basic [SVG](https://pyfpdf.github.io/fpdf2/SVG.html) import * Embedding [barcodes](https://pyfpdf.github.io/fpdf2/Barcodes.html), [charts & graphs](https://pyfpdf.github.io/fpdf2/Maths.html), [emojis, symbols & dingbats](https://pyfpdf.github.io/fpdf2/EmojisSymbolsDingbats.html) - * [Cell / multi-cell / plaintext writing](https://pyfpdf.github.io/fpdf2/Text.html), with [automatic page breaks](https://pyfpdf.github.io/fpdf2/PageBreaks.html), line break and text justification + * [Tables](https://pyfpdf.github.io/fpdf2/Tables.html) and also [cell / multi-cell / plaintext writing](https://pyfpdf.github.io/fpdf2/Text.html), with [automatic page breaks](https://pyfpdf.github.io/fpdf2/PageBreaks.html), line break and text justification * Choice of measurement unit, page format & margins. Optional page header and footer * Basic [conversion from HTML to PDF](https://pyfpdf.github.io/fpdf2/HTML.html) * A [templating system](https://pyfpdf.github.io/fpdf2/Templates.html) to render PDFs in batchs diff --git a/docs/index.md b/docs/index.md index 551f74935..5cf985596 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ Go try it **now** online in a Jupyter notebook: [![Open In Colab](https://colab. * Embedding images, including transparency and alpha channel, using [Pillow (Python Imaging Library)](https://pillow.readthedocs.io/en/stable/) * Arbitrary path drawing and basic [SVG](SVG.md) import * Embedding [barcodes](Barcodes.md), [charts & graphs](Maths.md), [emojis, symbols & dingbats](EmojisSymbolsDingbats.md) -* [Cell / multi-cell / plaintext writing](Text.md), with [automatic page breaks](PageBreaks.md), line break and text justification +* [Tables](Tables.md), and also [cell / multi-cell / plaintext writing](Text.md), with [automatic page breaks](PageBreaks.md), line break and text justification * Choice of measurement unit, page format & margins. Optional page header and footer * Basic [conversion from HTML to PDF](HTML.md) * A [templating system](Templates.md) to render PDFs in batchs diff --git a/fpdf/enums.py b/fpdf/enums.py index 8082d269f..2e58c812b 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -212,12 +212,13 @@ def style(self): @classmethod def coerce(cls, value): - if value.upper() == "BOLD": - return cls.B - if value.upper() == "ITALICS": - return cls.I - if value.upper() == "UNDERLINE": - return cls.U + if isinstance(value, str): + if value.upper() == "BOLD": + return cls.B + if value.upper() == "ITALICS": + return cls.I + if value.upper() == "UNDERLINE": + return cls.U return super(cls, cls).coerce(value) @@ -236,6 +237,9 @@ class TableBordersLayout(CoerciveEnum): MINIMAL = intern("MINIMAL") "Draw only the top horizontal border, below the headings, and internal vertical borders" + HORIZONTAL_LINES = intern("HORIZONTAL_LINES") + "Draw only horizontal lines" + NO_HORIZONTAL_LINES = intern("NO_HORIZONTAL_LINES") "Draw all cells border except horizontal lines, after the headings" diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 78005bf19..4056c94ec 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -1,6 +1,7 @@ """ Definition of the character widths of all PDF standard fonts. """ +from dataclasses import dataclass, replace from typing import Optional, Union from .drawing import DeviceGray, DeviceRGB @@ -2605,21 +2606,22 @@ } +@dataclass class FontStyle: + family: Optional[str] + emphasis: Optional[TextEmphasis] + size_pt: Optional[int] + # Colors are single number grey scales or (red, green, blue) tuples: + color: Optional[Union[int, tuple, DeviceGray, DeviceRGB]] + fill_color: Optional[Union[int, tuple, DeviceGray, DeviceRGB]] + def __init__( - self, - family: Optional[str] = None, - emphasis: Optional[Union[str, TextEmphasis]] = None, - size_pt: Optional[int] = None, - color: Optional[ - Union[int, tuple, DeviceGray, DeviceRGB] - ] = None, # grey scale or (red, green, blue) - fill_color: Optional[ - Union[int, tuple, DeviceGray, DeviceRGB] - ] = None, # grey scale or (red, green, blue) + self, family=None, emphasis=None, size_pt=None, color=None, fill_color=None ): self.family = family self.emphasis = TextEmphasis.coerce(emphasis) if emphasis else None self.size_pt = size_pt self.color = color self.fill_color = fill_color + + replace = replace diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 5dce32e15..d3de3559c 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -4512,6 +4512,9 @@ def _use_title_style(self, title_style: TitleStyle): @contextmanager def use_font_style(self, font_style: FontStyle): + if not font_style: + yield + return prev_font = (self.font_family, self.font_style, self.font_size_pt) self.set_font( font_style.family or self.font_family, diff --git a/fpdf/html.py b/fpdf/html.py index acb774217..d60a3caa8 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -9,7 +9,9 @@ import logging, warnings from html.parser import HTMLParser -from .enums import XPos, YPos +from .enums import TextEmphasis, XPos, YPos +from .fonts import FontStyle +from .table import Table import re @@ -227,7 +229,6 @@ def __init__( self.image_map = image_map or (lambda src: src) self.li_tag_indent = li_tag_indent self.dd_tag_indent = dd_tag_indent - self.table_line_separators = table_line_separators self.ul_bullet_char = ul_bullet_char self.style = dict(b=False, i=False, u=False) self.pre_formatted = False @@ -242,42 +243,46 @@ def __init__( self.font_size = pdf.font_size_pt self.set_font(pdf.font_family or "times", size=self.font_size) self.font_color = 0, 0, 0 # initialize font color, r,g,b format - self.table = None # table attributes - self.table_col_width = None # column (header) widths - self.table_col_index = None # current column index - self.td = None # inside a , attributes dict - self.thead = None # inside a , attributes dict - self.tfoot = None # inside a , attributes dict - self.tr_index = None # row index - self.theader = None # table header cells - self.tfooter = None # table footer cells - self.theader_out = self.tfooter_out = False - self.table_row_height = 0 self.heading_level = None self.heading_sizes = dict(**DEFAULT_HEADING_SIZES) self.heading_above = 0.2 # extra space above heading, relative to font size self.heading_below = 0.2 # extra space below heading, relative to font size if heading_sizes: self.heading_sizes.update(heading_sizes) - self._only_imgs_in_td = False self.warn_on_tags_not_matching = warn_on_tags_not_matching self._tags_stack = [] - - def width2unit(self, length): - "Handle conversion of % measures into the measurement unit used" - if length[-1] == "%": - total = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin - if self.table["width"][-1] == "%": - total *= int(self.table["width"][:-1]) / 100 - return int(length[:-1]) * total / 100 - return int(length) + #
` / `` tags anymore, nor `height` / `width` attributes defined on `` tags inside cells, nor `width` attributes defined on `` / `
, attributes dict - self.th = None # inside a , attributes dict - self.tr = None # inside a
-related properties: + self.table_line_separators = table_line_separators + self.table = None + self.table_row = None + self.tr = None + self.td_th = None def handle_data(self, data): trailing_space_flag = TRAILING_SPACE.search(data) - if self.td is not None: # drawing a table? - self._insert_td(data) + if self.td_th is not None: + data = data.strip() + if not data: + return + align = self.td_th.get("align", self.tr.get("align")) + if align: + align = align.upper() + bgcolor = color_as_decimal( + self.td_th.get("bgcolor", self.tr.get("bgcolor", None)) + ) + colspan = int(self.td_th.get("colspan", "1")) + emphasis = 0 + if self.td_th.get("b"): + emphasis |= TextEmphasis.B + if self.td_th.get("i"): + emphasis |= TextEmphasis.I + if self.td_th.get("U"): + emphasis |= TextEmphasis.U + style = None + if bgcolor or emphasis: + style = FontStyle(emphasis=emphasis, fill_color=bgcolor) + self.table_row.cell(text=data, align=align, style=style, colspan=colspan) + self.td_th["rendered"] = True elif self.table is not None: # ignore anything else than td inside a table pass @@ -331,154 +336,6 @@ def handle_data(self, data): self.pdf.write(self.h, data) self.follows_fmt_tag = False - def _insert_td(self, data=""): - self._only_imgs_in_td = False - width = self._td_width() - height = int(self.td.get("height", 0)) // 4 or self.h * 1.30 - if not self.table_row_height: - self.table_row_height = height - elif self.table_row_height > height: - height = self.table_row_height - border = int(self.table.get("border", 0)) - if self.th: - self.set_style("B", True) - border = border or "B" - align = self.td.get("align", "C")[0].upper() - else: - align = self.td.get("align", "L")[0].upper() - border = border and "LR" - bgcolor = color_as_decimal(self.td.get("bgcolor", self.tr.get("bgcolor", ""))) - # parsing table header/footer (drawn later): - if self.thead is not None: - self.theader.append( - ( - dict( - w=width, - h=height, - txt=data, - border=border, - new_x=XPos.RIGHT, - new_y=YPos.TOP, - align=align, - ), - bgcolor, - ) - ) - if self.tfoot is not None: - self.tfooter.append( - ( - dict( - w=width, - h=height, - txt=data, - border=border, - new_x=XPos.RIGHT, - new_y=YPos.TOP, - align=align, - ), - bgcolor, - ) - ) - # check if reached end of page, add table footer and header: - if self.tfooter: - height += self.tfooter[0][0]["h"] - if self.pdf.y + height > self.pdf.page_break_trigger and not self.th: - self.output_table_footer() - self.pdf.add_page(same=True) - self.theader_out = self.tfooter_out = False - if self.tfoot is None and self.thead is None: - if not self.theader_out: - self.output_table_header() - self.box_shadow(width, height, bgcolor) - # self.pdf.x may have shifted due to inside ', + width, + tag, + ) if tag == "img" and "src" in attrs: width = px2mm(int(attrs.get("width", 0))) height = px2mm(int(attrs.get("height", 0))) + if self.table_row: # => in a
: - self.pdf.set_x(self._td_x()) - LOGGER.debug( - "td cell x=%d width=%d height=%d border=%s align=%s '%s'", - self.pdf.x, - width, - height, - border, - align, - data.replace("\n", "\\n"), - ) - self.pdf.cell( - width, - height, - data, - border=border, - align=align, - new_x=XPos.RIGHT, - new_y=YPos.TOP, - ) - - def _td_x(self): - "Return the current table cell left side horizontal position" - prev_cells_total_width = sum( - self.width2unit(width) - for width in self.table_col_width[: self.table_col_index] - ) - return self.table_offset + prev_cells_total_width - - def _td_width(self): - "Return the current table cell width" - # pylint: disable=raise-missing-from - if "width" in self.td: - column_widths = [self.td["width"]] - elif "colspan" in self.td: - i = self.table_col_index - colspan = int(self.td["colspan"]) - column_widths = self.table_col_width[i : i + colspan] - else: - try: - column_widths = [self.table_col_width[self.table_col_index]] - except IndexError: - raise ValueError( - f"Width not specified for table column {self.table_col_index}," - " unable to continue" - ) - return sum(self.width2unit(width) for width in column_widths) - - def box_shadow(self, w, h, bgcolor): - LOGGER.debug("box_shadow w=%d h=%d bgcolor=%s", w, h, bgcolor) - if bgcolor: - fill_color = self.pdf.fill_color - self.pdf.set_fill_color(*bgcolor) - self.pdf.rect(self.pdf.x, self.pdf.y, w, h, "F") - self.pdf.set_fill_color(*fill_color.colors) - - def output_table_header(self): - if self.theader: - b = self.style.get("b") - self.pdf.set_x(self.table_offset) - self.set_style("b", True) - for celldict, bgcolor in self.theader: - self.box_shadow(celldict["w"], celldict["h"], bgcolor) - self.pdf.cell(**celldict) # includes the border - self.set_style("b", b) - self.pdf.ln(self.theader[0][0]["h"]) - self.pdf.set_x(self.table_offset) - # self.pdf.set_x(prev_x) - self.theader_out = True - - def output_table_footer(self): - if self.tfooter: - x = self.pdf.x - self.pdf.set_x(self.table_offset) - for celldict, bgcolor in self.tfooter: - self.box_shadow(celldict["w"], celldict["h"], bgcolor) - self.pdf.cell(**celldict) - self.pdf.ln(self.tfooter[0][0]["h"]) - self.pdf.set_x(x) - if self.table.get("border"): - self.output_table_sep() - self.tfooter_out = True - - def output_table_sep(self): - x1 = self.pdf.x - y1 = self.pdf.y - width = sum(self.width2unit(length) for length in self.table_col_width) - self.pdf.line(x1, y1, x1 + width, y1) - def handle_starttag(self, tag, attrs): attrs = dict(attrs) LOGGER.debug("STARTTAG %s %s", tag, attrs) @@ -494,7 +351,10 @@ def handle_starttag(self, tag, attrs): if tag == "em": tag = "i" if tag in ("b", "i", "u"): - self.set_style(tag, True) + if self.td_th is not None: + self.td_th[tag] = True + else: + self.set_style(tag, True) if tag == "a": self.href = attrs["href"] if tag == "br": @@ -562,72 +422,73 @@ def handle_starttag(self, tag, attrs): self.set_font() self.set_text_color(*self.font_color) if tag == "table": - self.table = {k.lower(): v for k, v in attrs.items()} - if "width" not in self.table: - self.table["width"] = "100%" - if self.table["width"][-1] == "%": - w = self.pdf.w - self.pdf.r_margin - self.pdf.l_margin - w *= int(self.table["width"][:-1]) / 100 - self.table_offset = (self.pdf.w - w) / 2 - self.table_col_width = [] - self.theader_out = self.tfooter_out = False - self.theader = [] - self.tfooter = [] - self.thead = None - self.tfoot = None + self.table = Table(self.pdf) + self.table.line_height = self.h * 1.30 + width = attrs.get("width") + if width: + if width[-1] == "%": + width = self.pdf.epw * int(width[:-1]) / 100 + else: + width = px2mm(int(width)) + self.table.width = width + if "border" in attrs: + self.table.borders_layout = ( + "ALL" if self.table_line_separators else "NO_HORIZONTAL_LINES" + ) + else: + self.table.borders_layout = ( + "HORIZONTAL_LINES" + if self.table_line_separators + else "SINGLE_TOP_LINE" + ) self.pdf.ln() if tag == "tr": - self.tr_index = 0 if self.tr_index is None else (self.tr_index + 1) self.tr = {k.lower(): v for k, v in attrs.items()} - self.table_col_index = 0 - self.table_row_height = 0 - self.pdf.set_x(self.table_offset) - # Adding an horizontal line separator between rows: - if self.table_line_separators and self.tr_index > 0: - self.output_table_sep() - if tag == "td": - self.td = {k.lower(): v for k, v in attrs.items()} - if "width" in self.td and self.table_col_index >= len(self.table_col_width): - assert self.table_col_index == len( - self.table_col_width - ), f"table_col_index={self.table_col_index} #table_col_width={len(self.table_col_width)}" - self.table_col_width.append(self.td["width"]) - if attrs: - self.align = attrs.get("align") - self._only_imgs_in_td = False - if tag == "th": - self.td = {k.lower(): v for k, v in attrs.items()} - self.th = True - if "width" in self.td and self.table_col_index >= len(self.table_col_width): - assert self.table_col_index == len( - self.table_col_width - ), f"table_col_index={self.table_col_index} #table_col_width={len(self.table_col_width)}" - self.table_col_width.append(self.td["width"]) - if tag == "thead": - self.thead = {} - if tag == "tfoot": - self.tfoot = {} + with self.table.row() as row: + self.table_row = row + if tag in ("td", "th"): + self.td_th = {k.lower(): v for k, v in attrs.items()} + if tag == "th": + self.td_th["align"] = "CENTER" + self.td_th["b"] = True + if "height" in attrs: + LOGGER.warning( + 'Ignoring unsupported height="%s" specified on a <%s>', + attrs["height"], + tag, + ) + if "width" in attrs: + width = attrs["width"] + if len(self.table.rows) == 1: # => first table row + if width[-1] == "%": + width = width[:-1] + if not self.table.col_widths: + self.table.col_widths = [] + self.table.col_widths.append(int(width)) + else: + LOGGER.warning( + 'Ignoring width="%s" specified on a <%s> that is not on the first
+ 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 b31838a392fcef84b891cf2aa392787f953dd112..b0d3c5fba7555bed99e8bf3f8c6385c807d7c097 100644 GIT binary patch delta 1572 zcmaE=_eXDoJ4?N#A(x#US8+*EYGN)|#hj&Kr^{v=h}?Ou{ch9U6;<5R&aU=7=Xq() zFLsgHH@pt0v@AK>{-s_!>C1r=X11xUJ!$V!-zh98sBD=S@Zt4~3by!joB~}2Z8vS_ zoHg6dB4D)RD2I~d3LfR7iQ5|d*-BXbeGB8wr{>*GV$A)x+jV|o{kI39ZHLY`2%gqt z&GGM;#57BlCDT>lzQ_8mO=}*mTl2jC{QA=9FS81MD=yrjF!#uWv#g6=HS^y6$j)?9 zPl#vZ!9|w@uD{IlT{YoIyUAgtz3Xm=UX;0Z)bi9##k(6;uv`|q>KHjQ*d=v>Do5L) zqUxI>!b=``JaL@*E`ay+vRewR^)8mYr*hhhBFs!4DJPa#@7injyl;y}+_T=wEZ@Ct z*S4f?RA1P5{>6gr{mM$G-i8)Vb-kDsysv`!&h9d4KjFQXc?=nqj?YbDRXM$T^1odz z^XC?_1Rt5YS5x5TmAw{s|6AM+ZQCTup}Bho?`eaR0_lY_vt!?W&h>gPo!okA;*E^@ zUvGS0b>6t4CHZ;(v@|*MtKu8>em7ny_~Hel?Yj5JQu5?em}k2-$v!AeIq~+2pm65r zNzCDECa-KLY72R`?r*c#k`;Sb-f8~#x~F}UXn}O_to*5oQxwlUHeye+SzD zcjf!DJ}p_NG2P?RjQzH$;Rp3n9vfQMIkYF2<+RmX_q!LDePMV%eN(Ychwt=w%6Z()1EJ|!8-H3-!uV^7nO^zbH3LUJU7?oApeW) zZp$}08;PON1M|GlDnvO%xsn{PvFS{RfZ@==R-@OuMI!fF6&Hq+uty+;IvWsDN+N1~1 z;+oY$xGq`WS(@Cb=XFci$oWA-&O+u*Y|b+Gbik!ws^UeeZGCMeTeR2#TAFO z+QovJ7CyN7uk_-}kh;m~qP+DMSmZ5@G2CEmU~GbJ zwz0V}hJ7YR78vd_F)=Yh*K1;GXa*9iNA|plnF)p^Cg$dtnk_8B9zfP>YG8yBn5KrN z7>+SDG6h=<(OWY4nwU|nk%^I+iJ7ybv!kJffs2`?sk6DGsiTprftjnRqlKe^oee=1 bv5*v5tp4ES8+*EYGN)|#hkriw}WmQh}1mSK9~G^(JQ^z6SKLE4K8Qa z9XQg)@kXL&;Zx^d`=<(@o-|o_(o3J&%WqVd74O;Yb?D)%4x!Ws&o7?j*%xDwFeiX( zv$e(iwfUL}IYr(Ehl~s)RoxD)Y4GR!BsK5Wr*)aH_q;yEwChv2d%fM^H;ExLx$K!$ z4;ArU*`Mg9o$+4LOWN&8R8?SDDD(DE9aN@~tilM^rBopndR_j~5Cg*7)zx;Ncj@2$HrX?~u}ssp8|d&9$`{dV+i>NyY+ zk>4kq)MPeELpt;)-&KpU6}I)WA! z#s}TQ%a}hJZS0O{c5&kOLYIDRR4Yd zp4&`1lMmL<&52fU)W22JS#f>~@4E<~0qb-7O51<#H)*!#(Tr!R*N%NCz?gXKlj2+N z2dB+-?2c7_=m4=-Uwedm8^t{~^>CQqK1@hXnv! zU=LUl@ItC0=hqa@y5LOSD%Nw+CdVZ+S94wx)4lp}OZNU-%XUp-Rp(p%hpSJ}%jR#4 z88fp)^DCALHjR(LQR~Zj7BKA+7LZtQdN0`Sa|4P+PaGd zPMyy)gXKOlpX%JdbMtqO9G-d$Qws$H5Kzcd-~uxY3@wc<(Z!66EDbQkjLnTO#7qs% zF~rQUh?yIjfz;I_TWn;3MWLmkDY}8i28MLBf#Kh7V z-9QsFBMeJS%uSFrL-bmhgMEVR5mTU%=;oOk8e=%d)QHL0c=97L+gJ-%LlZM+Qwvj9 z0~aGVOG9S^HxpA!GczM|V+&VTM@Kswf+}Jmi9WHYq@pM_jmy-?(v(Y8)z#mP3jk)` BW=;SA 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 6dd04631dca24e67e2a627e547ef256d8884255a..37d32850638ebcbda2dd3d3a7780f333ba6a4f6d 100644 GIT binary patch delta 428 zcmX@l`HpjgJ!8F*kr|hr9anKlQEFl?SH+yE6AtDbG7xZi|Fg^XP-^!2z1h#ZSlaKc z`o-_1q0q$Q^yqtG=$e$p9TOg(GX8Ai{2@~E3B&iSsSlEq4@#P_UE;g8a@Pv;pnQ>w zhAj^!+HOf`-LUR-ZBzb@1L2;SpxtAD=jfvh60Mq*$Hxe#Xcqi`I=yXcyt-)TC-*s&zbrM&)#I6~9+|F$6AEdPO=(e5Te5dNUMv780}@=|F8byW__ktjCLkw3I-sckf*=}W*8WlnV4dTnVT46 zh*_8zOb%zUj&X6burRkYGj?<~a5i%?HFI_}H!`zyHg$EhurxF=cD1u1s3H~;wuwb0 T6-B9OT$TogT&k+B{%%|VUUQ9D diff --git a/test/html/html_table_line_separators_issue_137.pdf b/test/html/html_table_line_separators_issue_137.pdf index d278ad893969eecb0c994dc5c198cfd9f8fafd4a..c6d470aa4d462ed52676cc2dd3b2d79893a482d2 100644 GIT binary patch delta 428 zcmX@k`I2*kJ!8F*ks+6z9anKlQEFl?SH+yE6L<3+GT?FjUf1d#;qz!uxQfILfuD_D zx=t&26d3pW%Ws*mWKrsuuM=z6`))Wbp~ZYY=WXKFrw2GCnd@76&#!arZ<31?;nERb zp%Sw|yVv-IN=QM&4t-Xs+t+mkG?*v$RB$IdT&A0)nXWV1nHwn>fPg}t0vDKJU|?o$h#_WfW{DwYVP-bjfW#&W50hSV%-97L`;KrKWLN8kuvcs=E5S GaRC4k!pvjZfuGnW?^hJIf%tN z#?Zjn#l_gf(9zh^%-qP)(!kZy#l_Ot*~Hk{+||k4)y{^XidaaPCKi=c6s4wdSy&o# Lsj9mAyKw;k(%XkV 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 f5ef356c9f464699203ae39e5ba935934cf5002d..b5aa7478e9b434f51cadf1f82ff832c2d4fbb132 100644 GIT binary patch delta 389 zcmX@gd6jd6J!8F*fgzWj9anKlQEFl?SH+yE6Hap-G7xb6{bByes6=TYP2DTbdiB<5^B zAtrUfDPmgTx`zyZ{p^4I>rD?4JH#ngTKaC9z`OYkocCMQ&gUM8kaCi$?vU+TvMBlM zi&fw3L*?!=bSGrGX=(GFUK3osJEnf>tYVGDfxrL72sxMX%()uRz1aMv-O<05OL-PA zE4i=Ix^I54p403DE95lPP1yKDlOG~$Z+|0)GiqX#0NWlOE6!H|fzzhQeGgCth zF>@133^5B6v&nia)-e{Y2BtJ)!&T^0H$|`IRF3v delta 373 zcmcc0d6aX5J!8G0g(;Vv9anKlQEFl?SH+yY{nmVk3KRNeD}NUs%N?E#+^N*A}`^&hv$-zIj_>=%Oj0+ zw-zXgob^dM8a(IC#k+df-z)8ZZ#t`_A>6LKWa9Nl8Z#ypIlld{``M&fO?^odCoGKZ z;(j&lQIprQ{Il;XH8zVey<)U8Hd8PF0fjsTE-=Huz|7bbL(JUR7(>j$*kH09i*<~v zvxT9Vxw)~Sft#g^vyr*0tEI8Cn}MUbk)xZ5iKT^|4M7#LkPuBQDyb++P2;k#G~`lM Jb@g}S0svU7iJ1TZ 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 2c061fbea44931dce9a16b009a95abb2eb3feb5f..c6050e45da4f29428072bb10b543150fa82b19af 100644 GIT binary patch delta 705 zcmcc4^NVML17p2~C6}EYS8+*EYGN)|#hj&6qO)&12(;e+s(mgx$isYByj->C!lfcU zEVXgBl{1AauTM*1|Nr}3lwa;nr6LWs&eUtg=YE!Lv8%IK*CDjWVt(NsWhy~7n;5glDenH~5Y2AspFYvy3edn0TvBmuQeJ)aSq)%!y z$(~&>N%xt{FTNoCg;90Ms`c}CCUJ$Vdh4@U-1#)myn6Sp$KCrSg5y~hOSDfBy1r}M zvwfHPvNrEKQgZF5Y32PYmb0!Og5S?y7Q!95c=eRJuw8o&M{Qec^lQRg(`d2$C2<1H z4`Pp$F17y9w)dvlo>ePfd&Cqr{Wq+vnQ&6{#G(_&wr{k{jC_ClU2O5yZlA-}E9-59 z42|}LOlmZcKIWJfD=Zdtza*nGV3q7vG0{_P6}SB&cZB<&f4G{jZTeArX3@%TZZnn$ zr#~`j*^5)-r*fs{r4*NJmShfQ3NkTJFaQCCJOwT=!@$7O*b-gL z(7@OnL(CA1n30Le$sGN^FMG~j z<{3Qk*Bce_0`E6GlS6)f+b!qIoO|U-q7&22nEBOp-{aJO7>*m1K^t~1G_EIB*beV%`pzvCs3w#({Q9{Y6elsP?}w{c$3f{Sw8O4Yo%;Rf^e zN4#eL_gKs1*!Oj}yPrQ&w5fMDS{GZ#I5X&AlE>evWmk5cT_MYUl=+-OrQGusKQCWR zTN(YOcO(C?Yu9gNyzkxozr$ks^Cfe)c_-QQDgDdO;pe-tJNNkNX}{(jnSHg6DU7Ft zaf1$%i^2Dkugc#4?zXb*nD1`C>b{Vnft>VN3!h^KPt0@^SH2D9_iwrN<^+4dl`z@D z8|PXsb)3{VeNtn8&ZdxuLUU66Er2k1*SNP=zrMHU)&B`^YXtjV?u%dh zF!p$Im$h2??K0NaRUvJ+*MC&M6BDvddROgxvGX758F$-j{@QHD9LW@9V5(pM0t$Hw zTwsQQfu(^7x|pGXff0t7Ar>(s1Ix(|S#4sS3`{Le%nc39T+GazEL<%Noh>ZP&7CZr m4Gb+!O$;3EYzV4|g(SMfqLPZD)HE(bBXa{TRaIAiH!c8WAQmJ5 diff --git a/test/html/html_table_with_border.pdf b/test/html/html_table_with_border.pdf index 89a8d12f46933e92a5bb9b98839c0fafc47b35b6..6c3f372f349773f38953261c4823772760514f2e 100644 GIT binary patch delta 506 zcmbQsb&_j?J!8GGp&6H*9anKlQEFl?SH+yElXm;D81lIO{>e4{!VH_aKHV&=vPu6K z$|^G)leP-&_jl_tE;RC9u%-C1%zbI*@@6Ob2iZI3J$Mz?9J)fpr+UZCHvhu3BQrZQ zY+Z6MT358sH)1iL&R)^*DrWM*!Y5T?C;Y-pl&2k=rjWQ)NzJ$Z%L*AE|AN9BZ~dnm z>^<8ft(}{5iG8K$IVqDQ$(tv;%G~ZP3so_B%q%qZwaq&_zuk}ji@iF0h0DKXv99=} zxoa}gpR`7~Iu(l)i2Rg^pC4bxy@IP&bM61n%UAS1tCF1-+C#r+vN+gPk)Ei4V43|&pl&0Jj^9WC9=%}szT q3ri;hGiO656H7BY8-glgA+efRR8motn#N^lU}VIls_N?R#svUK@W4m_ delta 462 zcmX@fHJ59HJ!8F*xiOcW9anKlQEFl?SH+yYfxG>V7zn)iEE?@}Fsb@&=^Z6U$L{|O zMK&ewf^(0&Pu9M7#H?IX^Z3l;=Z+lsyx>CR4@o(tJNB6htzM2c2a;pvow>ifK6R$x z$;R(ub6I`GZXT3v^_ihw(Wv5i|Cr0ho>NXW&9fhuSloB|bE@iWJ)5%t^I=Bc7rZ^6 zPsCX7@w@*#Q;gqRZFO1yhBt;L;b-Q*mwzoOerj6UlT9x<#ZELD+o%QajNxDn?@95J z`pOYk>3OyG;|L2sSkE_mVJ4va@)n%Y32&A2YHKH>m2R=F#V0b zSh~5B=@p}$k%@u<2q@$!aDf>H2IfY_7-AMih8SX&h8B}IvRKC$I61poI2&0wxfnW| xnHe}68W=dbni`uMm>3utnYozS*$`9_3yG@4qLPZD)HE(jb4xB&RabvEE&!(@uulL0 diff --git a/test/html/html_table_with_empty_cell_contents.pdf b/test/html/html_table_with_empty_cell_contents.pdf index efe61273cd75a7e5709690b3d803c034f4bea690..ff8f4a293cdac5630fbfa3f74c67f77fdd64a3d1 100644 GIT binary patch delta 459 zcmZqRn#HxjfwA7)fXmK~tGJ{nH8Gc~V$RY@r~Q}>d0fAL7L7Y}OvUewO0-JEhSewJ z4@_yd@(7vwqn6DybM8KoD@#k0=g0gOvf*X3JaE2X{=;_{n~XDfo~YV9dAR%Hii;YH zk~t14SI;=WmFTrK?x67*+hW1z<`XYgoLFD5S!(+1=o9nnnf*Ta)hFEADO}<1(Ju3- zK}qV1QC;q_um1zyzTk{tnQZM)$vH#H|8@WN@7~L*#GdNhvRLo^Dj=G@?C17F5_8`z zzjZ(Tlws7RzkZTh^NkmF#dV7A4Ub$|`XO%WDm|-9M-9!qwM!@K`MrEq^420XT6g&p z@5Xf=*ZD&EE-v!gC9+CCcDlIiR$GN`%jxSr|Fhn|*LJhgj>jDaHy3y4w8g7yefz)J zk?AF)y_u1rf&mC9W7%*1(|7NY4D#^yFZp^=#Q67=7_HB*C)S+O zvu!^6Nyv-8Y~{Cv=~16M|37RwcEKX>e6O(AM|Slvr`UY&6@C^+mI-SwvAXEAN|^6c zwz|sYX~LzJdQa;%D&5ij#%;}ZP}TADgL8913+n!1{s7#f+o8km_o8`;?qR1ph_j>Mvp TilWpsE=yAbE>%@me>W}w*tDQ6 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 22db3f56c913b25fc9c7796c06d6353b29174ae0..a1043948508c22068ea813c78e53ca271eb0d90f 100644 GIT binary patch delta 581 zcmcauyrN`-2_vK7WK%|$dP8F_J3Fr8lA_eaT&{{aQ_nbZ9WoGcy;vFcG3>*y&y)VB z1+R3n)=pt>GM?CXXCJGm$ea_haTbS!dQy+r%2>4pyNH~pJD^CReSOwCJ9E%$+wwW87(#^GS5*0QvXzynSo+~ zj2e@@G`lAEoAXZQR8!l`rqu;xSny1qV=lnPrSE2>U^aP$xr@BHf`LL1m%gWwf}sJ3 z3G^-yrRJsNCuMON8cpW1aA7o_>~E19&rS;eQH|X(XU^&yygu^^8Nk6I@2xoeSj* zr9T)JUn%y#zhL78v4Utr_0t!84hkl#^;yJhxWN?`{>FVvYoFiS=1=BThqSgWDO~c! z{->T|-rj>6g2#;x?#*@;FRG4uar#iy!*hAdp1L3G{LR37*rH_ecgCj4x0%e?EEEhB zf?_8-YN<`0pyoHZo0*5z93(XPp_;|!E6fQ>lg}|}Z;n^{$;4U3nfhuNVWQ1;> zp^32x#NNr1EXC`Mxb%ba^Gg(rpw7)p%U3WogmPRIqHPQeoz0CcoJ|azjV&yl%*`zw tT}@qFO)U*voy?uhO`PortB8dphs2_iilWpsE<+O&12ZmFRabvEE&!_+vV8ym 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 78983577fac863fc2217f3c63477ea3827f9a727..a1043948508c22068ea813c78e53ca271eb0d90f 100644 GIT binary patch delta 583 zcmX?7yrN`-2_vK7WK%|$dP8F_J3Fr8lA_eaT&{{aQ_nbZ9WoGcy;vFcG3>*y&y)VB z1+R3n)=pt>GM?CXXCJGm$ea_haTbS!dQy+r%2>4pyNH~pJD@>ReSPQCJ9E%$>*3_SuGR{6oMwtRkPSUfjL2m z(PHyQRb^(NTp**yWLwQHCPRbCA5{%E|J3Z_RWbys%}>hWva#XP2ODi;qwi*@UpjQN=9HEzs2&8kky`ps6!7FgG(s7c((3w*-svAuKjDF)^I1VJTa0 z%%vZcpI@S21ocE-TE2oIjN_sZZDZ+VZ0P1>U}kFS>}F(Q=3-`HZ077@Wa(t+XzpU_ gYGOxNMJyz7Bo>ua6s4wd0qr(7=2BI4^>^a}0Mc-$tpET3 delta 556 zcmZ2ca-?{J2_vJyWK%|$dJ|(VJ3Fr8lA_eaT&{{awSCrHOpXGqpDmAan^xYw``O=> z^=!+|#w3LfiOSmYfXIMrb&&s6LPJ-i`wMIg)D9^cpc>#sB^x$$PG>&WcO z>nwZuL3WzfTBl=~9XzaIFO-9{rnEd3J?yt}-$R3$o6np4G_7AFE0TC(v+R?cW>J4> z>8v!aj<}5X9J7_;-o?H6A7aDU``3bH@^{9j$+wxz*enzb6oO(WJ8G#-o}lJ8xtp1X z)f^-=`JtM{<}1tzN|VnqX>X2K`^m&;F*%S?V{)Tr*W?IIp2@eEf)Wu zqs24%in+k#cjgWvW+3fc`kqD#mLL`|4D9SC?>85n9ARO}Xf%0}MQ**Rp@IPjDC8+{ zff)t{W~K({VwUEHMrdM&2BwBU^HKB~8kkv{qKlar8KRqKXku&(v3K$$OYwRmF8!eV z{1OGA6=3J)rR6Ia0_DLR7lmjWOCvKQ0}C@3Cqpx1H&Y8EXA4scOH)^K6Bk!=M;8k> eJHjerAt@lSsHCDOHH{1C0!w2qRaIAiH!c8nwWsd@ 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 813841803a692f5f1aa29c4d3d7cd3d6b1bad615..1b53a4e87669472435a8dd2bceef2f919f302a74 100644 GIT binary patch delta 540 zcmbQVmT}cu#tjvW^%lllc6MCFB}J);xm*=^ZU+RUBA^OH}%^2^(+ zE#bnFM};K&J=H88NolS1P`5~Ep1sQ__|iv-?@ac_noVNPNuCKCE9#eN_)ai%v~i4k zlo&jfO};+k_hy6AbgAG+F`{?PTR~o#k}jf{tOAbb@OM6@J)VRyeaql z$vu*r6FOvDt5ZEMl*2D?ZdF9WnlPuRjDsj$EhK>fNF3!%z=0;}DmPUpKu8tOF qj*iYwCPpS^=H_-b1XaXBVm+~_q@pM_jmyZ;!qAXQRn^tsjSB#^sKM3% delta 491 zcmZ3rmT}Tr#tjvW^`@3wc6MCFB}J);xm*=N)yXe&7Oownq6Gxuj_XeRs3~){{52a z-t5@ckK3GweXR!!Pr|3J0# z!tv=Z4xIePZ^Y4NsUh}wO46gJd$Q}qV}0+Q6;eLYdDCEJM`U~&OKX07rjdH|%DoRk6G_%=!i}#60+vce!czs|IE+Rbs|*v|35VS#pVS}oWW^kW(o!% zppd7)1!fo+n46hmh*_8!qlp<>TAG-ki5VH18KCPmGBh_boy;F*8)x8ZWMJlG>}+IW z;AU#-X6fQ)VrgJtX6|O_>Sku-Y+`3aP(>^x5)+F`DvDCmxPWF`m~*MBy863u0RUoi Bwaowk 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): "
idnameidname
" @@ -110,8 +110,8 @@ def test_html_features(tmp_path): " " " " " " - ' id' - ' name' + " id" + " name" " " " " "" @@ -160,7 +160,7 @@ def getrow(i): " " " " " " - ' Alice' + ' Alice' " " ) + "".join(getrow(i) for i in range(26)) @@ -175,73 +175,6 @@ def getrow(i): assert_pdf_equal(pdf, HERE / "html_features.pdf", tmp_path) -def test_html_simple_table(tmp_path): - pdf = FPDF() - pdf.set_font_size(30) - pdf.add_page() - pdf.write_html( - """ - - - - - -
leftcenterright
123
456
""" - ) - assert_pdf_equal(pdf, HERE / "html_simple_table.pdf", tmp_path) - - -def test_html_table_line_separators(tmp_path): - pdf = FPDF() - pdf.set_font_size(30) - pdf.add_page() - pdf.write_html( - """ - - - - - -
leftcenterright
123
456
""", - table_line_separators=True, - ) - assert_pdf_equal(pdf, HERE / "html_table_line_separators.pdf", tmp_path) - - -def test_html_table_th_inside_tr_issue_137(tmp_path): - pdf = FPDF() - pdf.add_page() - pdf.write_html( - """ - - - - - - - - -
header1header2
value1value2
""" - ) - assert_pdf_equal(pdf, HERE / "html_table_line_separators_issue_137.pdf", tmp_path) - - -def test_html_table_with_border(tmp_path): - pdf = FPDF() - pdf.set_font_size(30) - pdf.add_page() - pdf.write_html( - """ - - - - - -
leftcenterright
123
456
""" - ) - assert_pdf_equal(pdf, HERE / "html_table_with_border.pdf", tmp_path) - - def test_html_bold_italic_underline(tmp_path): pdf = FPDF() pdf.set_font_size(30) @@ -255,7 +188,7 @@ def test_html_bold_italic_underline(tmp_path): assert_pdf_equal(pdf, HERE / "html_bold_italic_underline.pdf", tmp_path) -def test_customize_ul(tmp_path): +def test_html_customize_ul(tmp_path): html = """