Skip to content

Commit 5c078a9

Browse files
authored
Override heading style with supplied cell style (#979)
Add combine method to FontFace in fonts.py, allowing for partial overrides
1 parent d81018d commit 5c078a9

8 files changed

+110
-8
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
2424
### Fixed
2525
* Previously set dash patterns were not transferred correctly to new pages.
2626
* Inserted Vector images used to ignore the `keep_aspect_ratio` argument.
27+
* [`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.
28+
### Changed
29+
* [`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.
2730

2831
## [2.7.6] - 2023-10-11
2932
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
4043
* [`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.
4144
* [`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".
4245
* variable-width non-breaking space (NBSP) support [issue #834](https://github.com/PyFPDF/fpdf2/issues/834)
43-
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.
46+
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.
4447
### Fixed
4548
* [`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
4649
* [`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

docs/Tables.md

+19
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,25 @@ Result:
156156

157157
![](table-styled.jpg)
158158

159+
It's possible to override the style of individual cells in the heading. The overriding style will take
160+
precedence for any specified values, while retaining the default style for unspecified values:
161+
```python
162+
...
163+
headings_style = FontFace(emphasis="ITALICS", color=blue, fill_color=grey)
164+
override_style = FontFace(emphasis="BOLD")
165+
with pdf.table(headings_style=headings_style) as table:
166+
headings = table.row()
167+
headings.cell("First name", style=override_style)
168+
headings.cell("Last name", style=override_style)
169+
headings.cell("Age")
170+
headings.cell("City")
171+
...
172+
```
173+
174+
Result:
175+
176+
![](table-styled-override.jpg)
177+
159178
## Set cells background
160179

161180
```python

docs/table-styled-override.jpg

156 KB
Loading

fpdf/fonts.py

+34
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,40 @@ def __init__(
5959

6060
replace = replace
6161

62+
@staticmethod
63+
def _override(override_value, current_value):
64+
"""Override the current value if an override value is provided"""
65+
return current_value if override_value is None else override_value
66+
67+
@staticmethod
68+
def combine(override_style, default_style):
69+
"""
70+
Create a combined FontFace with all the supplied features of the two styles. When both
71+
the default and override styles provide a feature, prefer the override style.
72+
Override specified FontFace style features
73+
Override this FontFace's values with the values of `other`.
74+
Values of `other` that are None in this FontFace will be kept unchanged.
75+
"""
76+
if override_style is None:
77+
return default_style
78+
if default_style is None:
79+
return override_style
80+
if not isinstance(override_style, FontFace):
81+
raise TypeError(f"Cannot combine FontFace with {type(override_style)}")
82+
if not isinstance(default_style, FontFace):
83+
raise TypeError(f"Cannot combine FontFace with {type(default_style)}")
84+
return FontFace(
85+
family=FontFace._override(override_style.family, default_style.family),
86+
emphasis=FontFace._override(
87+
override_style.emphasis, default_style.emphasis
88+
),
89+
size_pt=FontFace._override(override_style.size_pt, default_style.size_pt),
90+
color=FontFace._override(override_style.color, default_style.color),
91+
fill_color=FontFace._override(
92+
override_style.fill_color, default_style.fill_color
93+
),
94+
)
95+
6296

6397
class CoreFont:
6498
# RAM usage optimization:

fpdf/table.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -382,9 +382,13 @@ def _render_table_cell(
382382
if not isinstance(text_align, (Align, str)):
383383
text_align = text_align[j]
384384
if i < self._num_heading_rows:
385-
style = self._headings_style
385+
# Get the style for this cell by overriding the row style with any provided
386+
# headings style, and overriding that with any provided cell style
387+
style = FontFace.combine(
388+
cell.style, FontFace.combine(self._headings_style, row.style)
389+
)
386390
else:
387-
style = cell.style or row.style
391+
style = FontFace.combine(cell.style, row.style)
388392
if style and style.fill_color:
389393
fill = True
390394
elif (

test/fonts/test_combine_fontface.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from fpdf.fonts import FontFace
2+
3+
4+
def test_combine_fontface():
5+
font1 = FontFace(family="helvetica", size_pt=12)
6+
# providing None override should return the default style
7+
assert FontFace.combine(override_style=None, default_style=font1) == font1
8+
# overriding a None style should return the override
9+
assert FontFace.combine(override_style=font1, default_style=None) == font1
10+
font2 = FontFace(size_pt=14)
11+
combined = FontFace.combine(override_style=font2, default_style=font1)
12+
assert isinstance(combined, FontFace)
13+
assert combined.family == "helvetica" # wasn't overridden
14+
assert combined.emphasis is None # wasn't specified by either
15+
assert combined.size_pt == 14 # was overridden
Binary file not shown.

test/table/test_table.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -351,14 +351,17 @@ def test_table_capture_font_settings(tmp_path):
351351
pdf = FPDF()
352352
pdf.add_page()
353353
pdf.set_font("Times", size=16)
354+
black = (0, 0, 0)
354355
lightblue = (173, 216, 230)
355-
with pdf.table() as table:
356-
for data_row in TABLE_DATA:
356+
with pdf.table(headings_style=FontFace(color=black, emphasis="B")) as table:
357+
for row_num, data_row in enumerate(TABLE_DATA):
357358
with pdf.local_context(text_color=lightblue):
358359
row = table.row()
359-
for i, datum in enumerate(data_row):
360-
pdf.font_style = "I" if i == 0 else ""
361-
row.cell(datum)
360+
for col_num, datum in enumerate(data_row):
361+
font_style = FontFace(
362+
emphasis="I" if row_num > 0 and col_num == 0 else None
363+
)
364+
row.cell(datum, style=font_style)
362365
assert_pdf_equal(pdf, HERE / "table_capture_font_settings.pdf", tmp_path)
363366

364367

@@ -631,3 +634,27 @@ def test_table_with_no_horizontal_lines_layout(tmp_path):
631634
HERE / "table_with_no_horizontal_lines_layout.pdf",
632635
tmp_path,
633636
)
637+
638+
639+
def test_table_with_heading_style_overrides(tmp_path):
640+
pdf = FPDF()
641+
pdf.set_font(family="helvetica", size=10)
642+
pdf.add_page()
643+
644+
with pdf.table(
645+
headings_style=FontFace(emphasis="B", size_pt=18), num_heading_rows=2
646+
) as table:
647+
# should be Helvetica bold size 18
648+
table.row().cell("Big Heading", colspan=3)
649+
second_header = table.row()
650+
# should be Helvetica bold size 14:
651+
second_header_style_1 = FontFace(size_pt=14)
652+
second_header.cell("First", style=second_header_style_1)
653+
# should be Times italic size 14
654+
second_header_style_2_3 = FontFace(family="times", emphasis="I", size_pt=14)
655+
second_header.cell("Second", style=second_header_style_2_3)
656+
second_header.cell("Third", style=second_header_style_2_3)
657+
# should be helvetica normal size 10
658+
table.row(("Some", "Normal", "Data"))
659+
660+
assert_pdf_equal(pdf, HERE / "table_with_heading_style_overrides.pdf", tmp_path)

0 commit comments

Comments
 (0)