Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

write_html: do not style header row if no <th> is present - fix #740 #766

Merged
merged 1 commit into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ in order to get warned about deprecated features used in your code.
This can also be enabled programmatically with `warnings.simplefilter('default', DeprecationWarning)`.

## [2.7.4] - Not released yet

### Added
- documentation on how to embed `graphs` and `charts` generated using `Pygal` lib: [documentation section](https://pyfpdf.github.io/fpdf2/Maths.html#using-pygal) - thanks to @ssavi-ict
### Changed
- [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) does not render the top row as a header, in bold with a line below, when no `<th>` are used, in order to be more backward-compatible with earlier versions of `fpdf2` - _cf._ [#740](https://github.com/PyFPDF/fpdf2/issues/740)

## [2.7.3] - 2023-04-03
### Fixed
Expand All @@ -29,7 +30,6 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
### Fixed
- custom fonts can be used with `FPDF.table()` without triggering a `TypeError: cannot pickle 'dict_keys' object` - thanks @aeris07 for the bug report
- the SVG parser now accepts `<rect>` with `width` / `height` defined as percents

### Added
- documentation on how to generate Code128 barcodes using the `python-barcode` lib: [documentation section](https://pyfpdf.github.io/fpdf2/Barcodes.html#Code128)

Expand Down
13 changes: 12 additions & 1 deletion fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from html.parser import HTMLParser

from .enums import TextEmphasis, XPos, YPos
from .errors import FPDFException
from .fonts import FontFace
from .table import Table
from .table import Table, TableBordersLayout

import re

Expand Down Expand Up @@ -447,13 +448,23 @@ def handle_starttag(self, tag, attrs):
)
self.pdf.ln()
if tag == "tr":
if not self.table:
raise FPDFException("Invalid HTML: <tr> used outside any <table>")
self.tr = {k.lower(): v for k, v in attrs.items()}
self.table_row = self.table.row()
if tag in ("td", "th"):
if not self.table_row:
raise FPDFException(f"Invalid HTML: <{tag}> used outside any <tr>")
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
elif len(self.table.rows) == 1 and not self.table_row.cells:
# => we are in the 1st <tr>, and the 1st cell is a <td>
# => we do not treat the first row as a header
# pylint: disable=protected-access
self.table._borders_layout = TableBordersLayout.NONE
self.table._first_row_as_headings = False
if "height" in attrs:
LOGGER.warning(
'Ignoring unsupported height="%s" specified on a <%s>',
Expand Down
6 changes: 5 additions & 1 deletion fpdf/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ def get_cell_border(self, i, j):
def _render_table_row(self, i, fill=False, **kwargs):
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)
row_height = (
max(sum(lines_heights) for lines_heights in lines_heights_per_cell)
if lines_heights_per_cell
else 0
)
j = 0
while j < len(row.cells):
cell_line_height = row_height / len(lines_heights_per_cell[j])
Expand Down
Binary file added test/html/html_table_simple.pdf
Binary file not shown.
Binary file added test/html/html_table_with_bgcolor.pdf
Binary file not shown.
Binary file added test/html/html_table_with_img.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added test/html/html_table_with_only_tds.pdf
Binary file not shown.
228 changes: 228 additions & 0 deletions test/html/test_html_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
from pathlib import Path

import pytest

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


HERE = Path(__file__).resolve().parent


def test_html_table_simple(tmp_path):
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(
"""<table><thead><tr>
<th width="25%">left</th><th width="50%">center</th><th width="25%">right</th>
</tr></thead><tbody><tr>
<td>1</td><td>2</td><td>3</td>
</tr><tr>
<td>4</td><td>5</td><td>6</td>
</tr></tbody></table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_simple.pdf", tmp_path)


def test_html_table_line_separators(tmp_path):
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(
"""<table><thead><tr>
<th width="25%">left</th><th width="50%">center</th><th width="25%">right</th>
</tr></thead><tbody><tr>
<td>1</td><td>2</td><td>3</td>
</tr><tr>
<td>4</td><td>5</td><td>6</td>
</tr></tbody></table>""",
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(
"""<table border="1">
<tr>
<th width="40%">header1</th>
<th width="60%">header2</th>
</tr>
<tr>
<th>value1</th>
<td>value2</td>
</tr>
</table>"""
)
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(
"""<table border="1"><thead><tr>
<th width="25%">left</th><th width="50%">center</th><th width="25%">right</th>
</tr></thead><tbody><tr>
<td>1</td><td>2</td><td>3</td>
</tr><tr>
<td>4</td><td>5</td><td>6</td>
</tr></tbody></table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_with_border.pdf", tmp_path)


def test_html_table_with_img(caplog, tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""<table>
<tr>
<td width="50%">
<img src="test/image/png_images/affc57dfffa5ec448a0795738d456018.png" height="235" width="435"/>
</td>
<td width="50%">
<img src="test/image/image_types/insert_images_insert_png.png" height="162" width="154"/>
</td>
</tr>
</table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_with_img.pdf", tmp_path)
assert 'Ignoring unsupported "width" / "height" set on <img> element' in caplog.text


def test_html_table_with_img_without_explicit_dimensions(tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""<table>
<tr>
<td width="50%">
<img src="test/image/png_images/affc57dfffa5ec448a0795738d456018.png"/>
</td>
<td width="50%">
<img src="test/image/image_types/insert_images_insert_png.png"/>
</td>
</tr>
</table>"""
)
assert_pdf_equal(
pdf, HERE / "html_table_with_img_without_explicit_dimensions.pdf", tmp_path
)


def test_html_table_with_imgs_captions_and_colspan(caplog, tmp_path):
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""<table border="1">
<tr>
<td colspan="2" align="center"><b>Side by side centered pictures and captions</b></td>
</tr>
<tr>
<td width="50%" align="center"><img src="docs/fpdf2-logo.png"/></td>
<td width="50%" align="center"><img src="docs/fpdf2-logo.png"/></td>
</tr>
<tr>
<td width="50%" align="center">left caption</td>
<td width="50%" align="center">right caption</td>
</tr>
</table>"""
)
assert_pdf_equal(
pdf, HERE / "html_table_with_imgs_captions_and_colspan.pdf", tmp_path
)
assert (
'Ignoring width="50%" specified on a <td> that is not in the first <tr>'
in caplog.text
)


def test_html_table_with_empty_cell_contents(tmp_path): # issue 349
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
# Reference table cells positions:
pdf.write_html(
"""<table><thead><tr>
<th width="25%">left</th><th width="50%">center</th><th width="25%">right</th>
</tr></thead><tbody><tr>
<td>1</td><td>2</td><td>3</td>
</tr><tr>
<td>4</td><td>5</td><td>6</td>
</tr></tbody></table>"""
)
# Table with empty cells:
pdf.write_html(
"""<table><thead><tr>
<th width="25%">left</th><th width="50%">center</th><th width="25%">right</th>
</tr></thead><tbody><tr>
<td>1</td><td></td><td>3</td>
</tr><tr>
<td></td><td>5</td><td></td>
</tr></tbody></table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_with_empty_cell_contents.pdf", tmp_path)


def test_html_table_with_bgcolor(tmp_path): # issue-512
pdf = FPDF()
pdf.add_page()
pdf.write_html(
"""<table>
<thead>
<tr>
<th width="25%">Career</th>
<th width="75%">Quote</th>
</tr>
</thead>
<tbody>
<tr bgcolor="grey"><td>Engineer</td><td>The engineer has been, and is, a maker of history.</td></tr>
<tr bgcolor="white"><td>Developer</td><td>Logical thinking, passion and perseverance is the paint on your palette.</td></tr>
<tr bgcolor="grey"><td>Analyst</td><td>Seeing what other people can't see gives you great vision.</td></tr>
<tr bgcolor="white"><td><i>None of the above</i></td><td>I'm sorry. We could not find a quote for your job.</td></tr>
</tbody>
</table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_with_bgcolor.pdf", tmp_path)


def test_html_table_with_only_tds(tmp_path): # issue-740
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
pdf.write_html(
"""<table><tr>
<td>left</td><td>center</td><td>right</td>
</tr><tr>
<td>1</td><td>2</td><td>3</td>
</tr><tr>
<td>4</td><td>5</td><td>6</td>
</tr></table>"""
)
assert_pdf_equal(pdf, HERE / "html_table_with_only_tds.pdf", tmp_path)


def test_html_table_invalid(caplog):
pdf = FPDF()
pdf.set_font_size(30)
pdf.add_page()
# OK with empty tables:
pdf.write_html("<table></table>")
pdf.write_html("<table><tr></tr></table>")
pdf.write_html("<table><tr><td></td></tr></table>")
# KO if some elements are missing:
with pytest.raises(FPDFException) as error:
pdf.write_html("<table><td></td></table>")
assert str(error.value) == "Invalid HTML: <td> used outside any <tr>"
with pytest.raises(FPDFException) as error:
pdf.write_html("<table><th></th></table>")
assert str(error.value) == "Invalid HTML: <th> used outside any <tr>"
with pytest.raises(FPDFException) as error:
pdf.write_html("<tr></tr>")
assert str(error.value) == "Invalid HTML: <tr> used outside any <table>"
assert caplog.text == ""