diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b0d5310..6ff8a8985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ### Fixed * Previously set dash patterns were not transferred correctly to new pages. * Inserted Vector images used to ignore the `keep_aspect_ratio` argument. +* [`FPDF.fonts.FontFace`](https://py-pdf.github.io/fpdf2/fpdf/fonts.html#fpdf.fonts.FontFace): Now has a static `combine` method that allows overriding a default FontFace (e.g. for specific cells in a table). Unspecified properties of the override FontFace retain the values of the default. +### Changed +* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): If cell styles are provided for cells in heading rows, combine the cell style as an override with the overall heading style. ## [2.7.6] - 2023-10-11 This release is the first performed from the [@py-pdf GitHub org](https://github.com/py-pdf), where `fpdf2` migrated. @@ -40,7 +43,7 @@ This release also marks the arrival of two new maintainers: Georg Mischler ([@gm * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): the formatting output has changed in some aspects. Vertical spacing around headings and paragraphs may be slightly different, and elements at the top of the page don't have any extra spacing above anymore. * [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): If the height of a row is governed by an image, then the default vertical alignment of the other cells is "center". This was "top". * variable-width non-breaking space (NBSP) support [issue #834](https://github.com/PyFPDF/fpdf2/issues/834) -This change was made for consistency between row-height governed by text or images. The old behaviour can be enforced using the new vertical alignment parameter. +This change was made for consistency between row-height governed by text or images. The old behaviour can be enforced using the new vertical alignment parameter. ### Fixed * [`FPDF.table()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table) & [`FPDF.multi_cell()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell): when some horizontal padding was set, the text was not given quite enough space - thanks to @gmischler * [`FPDF.write_html()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) can now handle formatting tags within paragraphs without adding extra line breaks (except in table cells for now) - thanks to @gmischler diff --git a/docs/Tables.md b/docs/Tables.md index 38cd1fa67..932eee3ef 100644 --- a/docs/Tables.md +++ b/docs/Tables.md @@ -156,6 +156,25 @@ Result: ![](table-styled.jpg) +It's possible to override the style of individual cells in the heading. The overriding style will take +precedence for any specified values, while retaining the default style for unspecified values: +```python +... +headings_style = FontFace(emphasis="ITALICS", color=blue, fill_color=grey) +override_style = FontFace(emphasis="BOLD") +with pdf.table(headings_style=headings_style) as table: + headings = table.row() + headings.cell("First name", style=override_style) + headings.cell("Last name", style=override_style) + headings.cell("Age") + headings.cell("City") + ... +``` + +Result: + +![](table-styled-override.jpg) + ## Set cells background ```python diff --git a/docs/table-styled-override.jpg b/docs/table-styled-override.jpg new file mode 100644 index 000000000..84460e43d Binary files /dev/null and b/docs/table-styled-override.jpg differ diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 6bace3abc..cbd94d523 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -59,6 +59,40 @@ def __init__( replace = replace + @staticmethod + def _override(override_value, current_value): + """Override the current value if an override value is provided""" + return current_value if override_value is None else override_value + + @staticmethod + def combine(override_style, default_style): + """ + Create a combined FontFace with all the supplied features of the two styles. When both + the default and override styles provide a feature, prefer the override style. + Override specified FontFace style features + Override this FontFace's values with the values of `other`. + Values of `other` that are None in this FontFace will be kept unchanged. + """ + if override_style is None: + return default_style + if default_style is None: + return override_style + if not isinstance(override_style, FontFace): + raise TypeError(f"Cannot combine FontFace with {type(override_style)}") + if not isinstance(default_style, FontFace): + raise TypeError(f"Cannot combine FontFace with {type(default_style)}") + return FontFace( + family=FontFace._override(override_style.family, default_style.family), + emphasis=FontFace._override( + override_style.emphasis, default_style.emphasis + ), + size_pt=FontFace._override(override_style.size_pt, default_style.size_pt), + color=FontFace._override(override_style.color, default_style.color), + fill_color=FontFace._override( + override_style.fill_color, default_style.fill_color + ), + ) + class CoreFont: # RAM usage optimization: diff --git a/fpdf/table.py b/fpdf/table.py index 36b8da1ac..075e560b1 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -382,9 +382,13 @@ def _render_table_cell( if not isinstance(text_align, (Align, str)): text_align = text_align[j] if i < self._num_heading_rows: - style = self._headings_style + # Get the style for this cell by overriding the row style with any provided + # headings style, and overriding that with any provided cell style + style = FontFace.combine( + cell.style, FontFace.combine(self._headings_style, row.style) + ) else: - style = cell.style or row.style + style = FontFace.combine(cell.style, row.style) if style and style.fill_color: fill = True elif ( diff --git a/test/fonts/test_combine_fontface.py b/test/fonts/test_combine_fontface.py new file mode 100644 index 000000000..6faf76b5b --- /dev/null +++ b/test/fonts/test_combine_fontface.py @@ -0,0 +1,15 @@ +from fpdf.fonts import FontFace + + +def test_combine_fontface(): + font1 = FontFace(family="helvetica", size_pt=12) + # providing None override should return the default style + assert FontFace.combine(override_style=None, default_style=font1) == font1 + # overriding a None style should return the override + assert FontFace.combine(override_style=font1, default_style=None) == font1 + font2 = FontFace(size_pt=14) + combined = FontFace.combine(override_style=font2, default_style=font1) + assert isinstance(combined, FontFace) + assert combined.family == "helvetica" # wasn't overridden + assert combined.emphasis is None # wasn't specified by either + assert combined.size_pt == 14 # was overridden diff --git a/test/table/table_with_heading_style_overrides.pdf b/test/table/table_with_heading_style_overrides.pdf new file mode 100644 index 000000000..c62a705cd Binary files /dev/null and b/test/table/table_with_heading_style_overrides.pdf differ diff --git a/test/table/test_table.py b/test/table/test_table.py index aa030c6b0..25106f759 100644 --- a/test/table/test_table.py +++ b/test/table/test_table.py @@ -351,14 +351,17 @@ def test_table_capture_font_settings(tmp_path): pdf = FPDF() pdf.add_page() pdf.set_font("Times", size=16) + black = (0, 0, 0) lightblue = (173, 216, 230) - with pdf.table() as table: - for data_row in TABLE_DATA: + with pdf.table(headings_style=FontFace(color=black, emphasis="B")) as table: + for row_num, data_row in enumerate(TABLE_DATA): with pdf.local_context(text_color=lightblue): row = table.row() - for i, datum in enumerate(data_row): - pdf.font_style = "I" if i == 0 else "" - row.cell(datum) + for col_num, datum in enumerate(data_row): + font_style = FontFace( + emphasis="I" if row_num > 0 and col_num == 0 else None + ) + row.cell(datum, style=font_style) assert_pdf_equal(pdf, HERE / "table_capture_font_settings.pdf", tmp_path) @@ -631,3 +634,27 @@ def test_table_with_no_horizontal_lines_layout(tmp_path): HERE / "table_with_no_horizontal_lines_layout.pdf", tmp_path, ) + + +def test_table_with_heading_style_overrides(tmp_path): + pdf = FPDF() + pdf.set_font(family="helvetica", size=10) + pdf.add_page() + + with pdf.table( + headings_style=FontFace(emphasis="B", size_pt=18), num_heading_rows=2 + ) as table: + # should be Helvetica bold size 18 + table.row().cell("Big Heading", colspan=3) + second_header = table.row() + # should be Helvetica bold size 14: + second_header_style_1 = FontFace(size_pt=14) + second_header.cell("First", style=second_header_style_1) + # should be Times italic size 14 + second_header_style_2_3 = FontFace(family="times", emphasis="I", size_pt=14) + second_header.cell("Second", style=second_header_style_2_3) + second_header.cell("Third", style=second_header_style_2_3) + # should be helvetica normal size 10 + table.row(("Some", "Normal", "Data")) + + assert_pdf_equal(pdf, HERE / "table_with_heading_style_overrides.pdf", tmp_path)