Skip to content

Commit eeef988

Browse files
visheshdvivediandersonhcLucas-C
authored
Added TextStyle Horizontal Alignment (#1300)
Co-authored-by: Anderson Herzogenrath da Costa <andersonhc@gmail.com> Co-authored-by: Lucas Cimon <925560+Lucas-C@users.noreply.github.com>
1 parent 4e1ffcb commit eeef988

12 files changed

+107
-28
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
2424
* Python 3.13 is now officially supported
2525
* support for [page labels](https://py-pdf.github.io/fpdf2/PageLabels.html) and created a [reference table of contents](https://py-pdf.github.io/fpdf2/DocumentOutlineAndTableOfContents.html) implementation
2626
* documentation on how to: [render spreadsheets as PDF tables](https://py-pdf.github.io/fpdf2/RenderingSpreadsheetsAsPDFTables.html)
27+
* support for passing `Align` values (along with string values like `'C'`, `'L'`, `'R'`) in `l_margin` of `TextStyle` to horizontally align text [issue #1282](https://github.com/py-pdf/fpdf2/issues/1282)
28+
2729
### Fixed
2830
* support for `align=` in [`FPDF.table()`](https://py-pdf.github.io/fpdf2/Tables.html#setting-table-column-widths). Due to this correction, tables are now properly horizontally aligned on the page by default. This was always specified in the documentation, but was not in effect until now. You can revert to have left-aligned tables by passing `align="LEFT"` to `FPDF.table()`.
2931
* `FPDF.set_text_shaping(False)` was broken since version 2.7.8 and is now working properly - [issue #1287](https://github.com/py-pdf/fpdf2/issues/1287)

fpdf/enums.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,13 @@ class Align(CoerciveEnum):
192192
J = intern("JUSTIFY")
193193
"Justify text"
194194

195+
# pylint: disable=arguments-differ
195196
@classmethod
196-
def coerce(cls, value, case_sensitive=False):
197+
def coerce(cls, value):
197198
if value == "":
198199
return cls.L
200+
if isinstance(value, str):
201+
value = value.upper()
199202
return super(cls, cls).coerce(value)
200203

201204

@@ -212,8 +215,9 @@ class VAlign(CoerciveEnum):
212215
B = intern("BOTTOM")
213216
"Place text at the bottom of the cell, but obey the cells padding"
214217

218+
# pylint: disable=arguments-differ
215219
@classmethod
216-
def coerce(cls, value, case_sensitive=False):
220+
def coerce(cls, value):
217221
if value == "":
218222
return cls.M
219223
return super(cls, cls).coerce(value)
@@ -399,8 +403,9 @@ class TableCellFillMode(CoerciveEnum):
399403
EVEN_COLUMNS = intern("EVEN_COLUMNS")
400404
"Fill only table cells in even columns"
401405

406+
# pylint: disable=arguments-differ
402407
@classmethod
403-
def coerce(cls, value, case_sensitive=False):
408+
def coerce(cls, value):
404409
"Any class that has a .should_fill_cell() method is considered a valid 'TableCellFillMode' (duck-typing)"
405410
if callable(getattr(value, "should_fill_cell", None)):
406411
return value
@@ -471,8 +476,9 @@ def is_draw(self):
471476
def is_fill(self):
472477
return self in (self.F, self.DF)
473478

479+
# pylint: disable=arguments-differ
474480
@classmethod
475-
def coerce(cls, value, case_sensitive=False):
481+
def coerce(cls, value):
476482
if not value:
477483
return cls.D
478484
if value == "FD":

fpdf/fonts.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __deepcopy__(self, _memo):
3333

3434
from .deprecation import get_stack_level
3535
from .drawing import convert_to_device_color, DeviceGray, DeviceRGB
36-
from .enums import FontDescriptorFlags, TextEmphasis
36+
from .enums import FontDescriptorFlags, TextEmphasis, Align
3737
from .syntax import Name, PDFObject
3838
from .util import escape_parens
3939

@@ -125,7 +125,7 @@ def __init__(
125125
fill_color: Union[int, tuple] = None, # grey scale or (red, green, blue),
126126
underline: bool = False,
127127
t_margin: Optional[int] = None,
128-
l_margin: Optional[int] = None,
128+
l_margin: Union[Optional[int], Optional[Align], Optional[str]] = None,
129129
b_margin: Optional[int] = None,
130130
):
131131
super().__init__(
@@ -136,7 +136,14 @@ def __init__(
136136
fill_color,
137137
)
138138
self.t_margin = t_margin or 0
139-
self.l_margin = l_margin or 0
139+
140+
if isinstance(l_margin, (int, float)):
141+
self.l_margin = l_margin
142+
elif l_margin:
143+
self.l_margin = Align.coerce(l_margin)
144+
else:
145+
self.l_margin = 0
146+
140147
self.b_margin = b_margin or 0
141148

142149
def __repr__(self):

fpdf/fpdf.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -5212,6 +5212,10 @@ def start_section(self, name, level=0, strict=True):
52125212
if text_style.size_pt is not None:
52135213
prev_font_size_pt = self.font_size_pt
52145214
self.font_size_pt = text_style.size_pt
5215+
# check if l_margin value is of type Align or string
5216+
align = Align.L
5217+
if isinstance(text_style.l_margin, (Align, str)):
5218+
align = Align.coerce(text_style.l_margin)
52155219
page_break_triggered = self.multi_cell(
52165220
w=self.epw,
52175221
h=self.font_size,
@@ -5220,9 +5224,14 @@ def start_section(self, name, level=0, strict=True):
52205224
new_y=YPos.NEXT,
52215225
dry_run=True, # => does not produce any output
52225226
output=MethodReturnValue.PAGE_BREAK,
5227+
align=align,
52235228
padding=Padding(
52245229
top=text_style.t_margin or 0,
5225-
left=text_style.l_margin or 0,
5230+
left=(
5231+
text_style.l_margin
5232+
if isinstance(text_style.l_margin, (int, float))
5233+
else 0
5234+
),
52265235
bottom=text_style.b_margin or 0,
52275236
),
52285237
)
@@ -5238,24 +5247,38 @@ def start_section(self, name, level=0, strict=True):
52385247
w=self.epw,
52395248
h=self.font_size,
52405249
text=name,
5250+
align=align,
52415251
new_x=XPos.LMARGIN,
52425252
new_y=YPos.NEXT,
5253+
center=text_style.l_margin == Align.C,
52435254
)
52445255
self._outline.append(
52455256
OutlineSection(name, level, self.page, dest, outline_struct_elem)
52465257
)
52475258

52485259
@contextmanager
52495260
def use_text_style(self, text_style: TextStyle):
5261+
prev_l_margin = None
52505262
if text_style:
52515263
if text_style.t_margin:
52525264
self.ln(text_style.t_margin)
52535265
if text_style.l_margin:
5254-
self.set_x(text_style.l_margin)
5266+
if isinstance(text_style.l_margin, (float, int)):
5267+
prev_l_margin = self.l_margin
5268+
self.l_margin = text_style.l_margin
5269+
self.x = self.l_margin
5270+
else:
5271+
LOGGER.debug(
5272+
"Unsupported '%s' value provided as l_margin to .use_text_style()",
5273+
text_style.l_margin,
5274+
)
52555275
with self.use_font_face(text_style):
52565276
yield
52575277
if text_style and text_style.b_margin:
52585278
self.ln(text_style.b_margin)
5279+
if prev_l_margin is not None:
5280+
self.l_margin = prev_l_margin
5281+
self.x = self.l_margin
52595282

52605283
@contextmanager
52615284
def use_font_face(self, font_face: FontFace):

fpdf/html.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
b_margin=0.4,
4646
font_size_pt=30,
4747
t_margin=6,
48-
# center=True, - Enable this once #1282 is implemented
48+
l_margin="Center",
4949
),
5050
"h1": TextStyle(
5151
color="#960000", b_margin=0.4, font_size_pt=24, t_margin=5 + 834 / 900
@@ -514,10 +514,15 @@ def _new_paragraph(
514514
# due to the behaviour of TextRegion._render_column_lines()
515515
self._end_paragraph()
516516
self.align = align or ""
517+
if isinstance(indent, Align):
518+
# Explicit alignement takes priority over alignement provided as TextStyle.l_margin:
519+
if not self.align:
520+
self.align = indent
521+
indent = 0
517522
if not top_margin and not self.follows_heading:
518523
top_margin = self.font_size_pt / self.pdf.k
519524
self._paragraph = self._column.paragraph(
520-
text_align=align,
525+
text_align=self.align,
521526
line_height=line_height,
522527
skip_leading_spaces=True,
523528
top_margin=top_margin,
@@ -1187,7 +1192,11 @@ def _scale_units(pdf, in_tag_styles):
11871192
if isinstance(tag_style, TextStyle):
11881193
out_tag_styles[tag_name] = tag_style.replace(
11891194
t_margin=tag_style.t_margin * conversion_factor,
1190-
l_margin=tag_style.l_margin * conversion_factor,
1195+
l_margin=(
1196+
tag_style.l_margin * conversion_factor
1197+
if isinstance(tag_style.l_margin, (int, float))
1198+
else tag_style.l_margin
1199+
),
11911200
b_margin=tag_style.b_margin * conversion_factor,
11921201
)
11931202
else:

fpdf/outline.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,27 @@ class TableOfContents:
119119
to `FPDF.insert_toc_placeholder()`.
120120
"""
121121

122-
def __init__(self):
123-
self.text_style = TextStyle()
124-
self.use_section_title_styles = False
125-
self.level_indent = 7.5
126-
self.line_spacing = 1.5
127-
self.ignore_pages_before_toc = True
122+
def __init__(
123+
self,
124+
text_style: Optional[TextStyle] = None,
125+
use_section_title_styles=False,
126+
level_indent=7.5,
127+
line_spacing=1.5,
128+
ignore_pages_before_toc=True,
129+
):
130+
self.text_style = text_style or TextStyle()
131+
self.use_section_title_styles = use_section_title_styles
132+
self.level_indent = level_indent
133+
self.line_spacing = line_spacing
134+
self.ignore_pages_before_toc = ignore_pages_before_toc
128135

129136
def get_text_style(self, pdf: "FPDF", item: OutlineSection):
130137
if self.use_section_title_styles and pdf.section_title_styles[item.level]:
131138
return pdf.section_title_styles[item.level]
139+
if isinstance(self.text_style.l_margin, (str, Align)):
140+
raise ValueError(
141+
f"Unsupported l_margin value provided as TextStyle: {self.text_style.l_margin}"
142+
)
132143
return self.text_style
133144

134145
def render_toc_item(self, pdf: "FPDF", item: OutlineSection):
@@ -137,10 +148,10 @@ def render_toc_item(self, pdf: "FPDF", item: OutlineSection):
137148

138149
# render the text on the left
139150
with pdf.use_text_style(self.get_text_style(pdf, item)):
140-
indent = (item.level * self.level_indent) + pdf.l_margin
141-
pdf.set_x(indent)
151+
indent = item.level * self.level_indent
152+
pdf.set_x(pdf.l_margin + indent)
142153
pdf.multi_cell(
143-
w=pdf.w - indent - pdf.r_margin,
154+
w=pdf.epw - indent,
144155
text=item.name,
145156
new_x=XPos.END,
146157
new_y=YPos.LAST,
1 Byte
Binary file not shown.

test/outline/test_outline.py

+28-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from fpdf import FPDF, TextStyle, TitleStyle, errors
6+
from fpdf.enums import Align
67
from fpdf.outline import TableOfContents
78

89
from test.conftest import LOREM_IPSUM, assert_pdf_equal
@@ -67,6 +68,31 @@ def test_incoherent_start_section_hierarchy():
6768
pdf.start_section("Subtitle", level=2)
6869

6970

71+
def test_start_section_horizontal_alignment(tmp_path): # issue-1282
72+
pdf = FPDF()
73+
pdf.add_page()
74+
pdf.set_font("Helvetica", "", 20)
75+
76+
# left align
77+
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.L)
78+
pdf.set_section_title_styles(level0)
79+
pdf.start_section("left aligned section")
80+
81+
# center align
82+
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.C)
83+
pdf.set_section_title_styles(level0)
84+
pdf.start_section("center aligned section")
85+
86+
# right align
87+
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.R)
88+
pdf.set_section_title_styles(level0)
89+
pdf.start_section("right aligned section")
90+
91+
assert_pdf_equal(
92+
pdf, HERE / "test_start_section_horizontal_alignment.pdf", tmp_path
93+
)
94+
95+
7096
def test_set_section_title_styles_with_invalid_arg_type():
7197
pdf = FPDF()
7298
with pytest.raises(TypeError):
@@ -522,7 +548,6 @@ def footer():
522548
pdf.cell(text=pdf.get_page_label(), center=True)
523549

524550
for test_number in range(3):
525-
526551
pdf = FPDF()
527552
pdf.footer = footer
528553

@@ -584,13 +609,10 @@ def footer():
584609
pdf.ln()
585610
pdf.add_font(
586611
family="Quicksand",
587-
style="",
588612
fname=HERE.parent / "fonts" / "Quicksand-Regular.otf",
589613
)
590614
toc = TableOfContents()
591-
toc.text_style = TextStyle(
592-
font_family="Quicksand", font_style="", font_size_pt=14
593-
)
615+
toc.text_style = TextStyle(font_family="Quicksand", font_size_pt=14)
594616
pdf.insert_toc_placeholder(toc.render_toc, allow_extra_pages=True)
595617

596618
if test_number == 2:
@@ -605,8 +627,7 @@ def footer():
605627
)
606628
pdf.ln()
607629
pdf.ln()
608-
toc = TableOfContents()
609-
toc.use_section_title_styles = True
630+
toc = TableOfContents(use_section_title_styles=True)
610631
pdf.insert_toc_placeholder(toc.render_toc, allow_extra_pages=True)
611632

612633
pdf.set_page_label(label_style="D")
Binary file not shown.
-22 Bytes
Binary file not shown.
-21 Bytes
Binary file not shown.
-22 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)