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

Added TextStyle Horizontal Alignment #1300

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
* Python 3.13 is now officially supported
* 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
* documentation on how to: [render spreadsheets as PDF tables](https://py-pdf.github.io/fpdf2/RenderingSpreadsheetsAsPDFTables.html)
* 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)

### Fixed
* 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()`.
* `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)
Expand Down
14 changes: 10 additions & 4 deletions fpdf/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,13 @@ class Align(CoerciveEnum):
J = intern("JUSTIFY")
"Justify text"

# pylint: disable=arguments-differ
@classmethod
def coerce(cls, value, case_sensitive=False):
def coerce(cls, value):
if value == "":
return cls.L
if isinstance(value, str):
value = value.upper()
return super(cls, cls).coerce(value)


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

# pylint: disable=arguments-differ
@classmethod
def coerce(cls, value, case_sensitive=False):
def coerce(cls, value):
if value == "":
return cls.M
return super(cls, cls).coerce(value)
Expand Down Expand Up @@ -399,8 +403,9 @@ class TableCellFillMode(CoerciveEnum):
EVEN_COLUMNS = intern("EVEN_COLUMNS")
"Fill only table cells in even columns"

# pylint: disable=arguments-differ
@classmethod
def coerce(cls, value, case_sensitive=False):
def coerce(cls, value):
"Any class that has a .should_fill_cell() method is considered a valid 'TableCellFillMode' (duck-typing)"
if callable(getattr(value, "should_fill_cell", None)):
return value
Expand Down Expand Up @@ -471,8 +476,9 @@ def is_draw(self):
def is_fill(self):
return self in (self.F, self.DF)

# pylint: disable=arguments-differ
@classmethod
def coerce(cls, value, case_sensitive=False):
def coerce(cls, value):
if not value:
return cls.D
if value == "FD":
Expand Down
13 changes: 10 additions & 3 deletions fpdf/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __deepcopy__(self, _memo):

from .deprecation import get_stack_level
from .drawing import convert_to_device_color, DeviceGray, DeviceRGB
from .enums import FontDescriptorFlags, TextEmphasis
from .enums import FontDescriptorFlags, TextEmphasis, Align
from .syntax import Name, PDFObject
from .util import escape_parens

Expand Down Expand Up @@ -125,7 +125,7 @@ def __init__(
fill_color: Union[int, tuple] = None, # grey scale or (red, green, blue),
underline: bool = False,
t_margin: Optional[int] = None,
l_margin: Optional[int] = None,
l_margin: Union[Optional[int], Optional[Align], Optional[str]] = None,
b_margin: Optional[int] = None,
):
super().__init__(
Expand All @@ -136,7 +136,14 @@ def __init__(
fill_color,
)
self.t_margin = t_margin or 0
self.l_margin = l_margin or 0

if isinstance(l_margin, (int, float)):
self.l_margin = l_margin
elif l_margin:
self.l_margin = Align.coerce(l_margin)
else:
self.l_margin = 0

self.b_margin = b_margin or 0

def __repr__(self):
Expand Down
27 changes: 25 additions & 2 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5212,6 +5212,10 @@ def start_section(self, name, level=0, strict=True):
if text_style.size_pt is not None:
prev_font_size_pt = self.font_size_pt
self.font_size_pt = text_style.size_pt
# check if l_margin value is of type Align or string
align = Align.L
if isinstance(text_style.l_margin, (Align, str)):
align = Align.coerce(text_style.l_margin)
page_break_triggered = self.multi_cell(
w=self.epw,
h=self.font_size,
Expand All @@ -5220,9 +5224,14 @@ def start_section(self, name, level=0, strict=True):
new_y=YPos.NEXT,
dry_run=True, # => does not produce any output
output=MethodReturnValue.PAGE_BREAK,
align=align,
padding=Padding(
top=text_style.t_margin or 0,
left=text_style.l_margin or 0,
left=(
text_style.l_margin
if isinstance(text_style.l_margin, (int, float))
else 0
),
bottom=text_style.b_margin or 0,
),
)
Expand All @@ -5238,24 +5247,38 @@ def start_section(self, name, level=0, strict=True):
w=self.epw,
h=self.font_size,
text=name,
align=align,
new_x=XPos.LMARGIN,
new_y=YPos.NEXT,
center=text_style.l_margin == Align.C,
)
self._outline.append(
OutlineSection(name, level, self.page, dest, outline_struct_elem)
)

@contextmanager
def use_text_style(self, text_style: TextStyle):
prev_l_margin = None
if text_style:
if text_style.t_margin:
self.ln(text_style.t_margin)
if text_style.l_margin:
self.set_x(text_style.l_margin)
if isinstance(text_style.l_margin, (float, int)):
prev_l_margin = self.l_margin
self.l_margin = text_style.l_margin
self.x = self.l_margin
else:
LOGGER.debug(
"Unsupported '%s' value provided as l_margin to .use_text_style()",
text_style.l_margin,
)
with self.use_font_face(text_style):
yield
if text_style and text_style.b_margin:
self.ln(text_style.b_margin)
if prev_l_margin is not None:
self.l_margin = prev_l_margin
self.x = self.l_margin

@contextmanager
def use_font_face(self, font_face: FontFace):
Expand Down
15 changes: 12 additions & 3 deletions fpdf/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
b_margin=0.4,
font_size_pt=30,
t_margin=6,
# center=True, - Enable this once #1282 is implemented
l_margin="Center",
),
"h1": TextStyle(
color="#960000", b_margin=0.4, font_size_pt=24, t_margin=5 + 834 / 900
Expand Down Expand Up @@ -514,10 +514,15 @@ def _new_paragraph(
# due to the behaviour of TextRegion._render_column_lines()
self._end_paragraph()
self.align = align or ""
if isinstance(indent, Align):
# Explicit alignement takes priority over alignement provided as TextStyle.l_margin:
if not self.align:
self.align = indent
indent = 0
if not top_margin and not self.follows_heading:
top_margin = self.font_size_pt / self.pdf.k
self._paragraph = self._column.paragraph(
text_align=align,
text_align=self.align,
line_height=line_height,
skip_leading_spaces=True,
top_margin=top_margin,
Expand Down Expand Up @@ -1187,7 +1192,11 @@ def _scale_units(pdf, in_tag_styles):
if isinstance(tag_style, TextStyle):
out_tag_styles[tag_name] = tag_style.replace(
t_margin=tag_style.t_margin * conversion_factor,
l_margin=tag_style.l_margin * conversion_factor,
l_margin=(
tag_style.l_margin * conversion_factor
if isinstance(tag_style.l_margin, (int, float))
else tag_style.l_margin
),
b_margin=tag_style.b_margin * conversion_factor,
)
else:
Expand Down
29 changes: 20 additions & 9 deletions fpdf/outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,27 @@ class TableOfContents:
to `FPDF.insert_toc_placeholder()`.
"""

def __init__(self):
self.text_style = TextStyle()
self.use_section_title_styles = False
self.level_indent = 7.5
self.line_spacing = 1.5
self.ignore_pages_before_toc = True
def __init__(
self,
text_style: Optional[TextStyle] = None,
use_section_title_styles=False,
level_indent=7.5,
line_spacing=1.5,
ignore_pages_before_toc=True,
):
self.text_style = text_style or TextStyle()
self.use_section_title_styles = use_section_title_styles
self.level_indent = level_indent
self.line_spacing = line_spacing
self.ignore_pages_before_toc = ignore_pages_before_toc

def get_text_style(self, pdf: "FPDF", item: OutlineSection):
if self.use_section_title_styles and pdf.section_title_styles[item.level]:
return pdf.section_title_styles[item.level]
if isinstance(self.text_style.l_margin, (str, Align)):
raise ValueError(
f"Unsupported l_margin value provided as TextStyle: {self.text_style.l_margin}"
)
return self.text_style

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

# render the text on the left
with pdf.use_text_style(self.get_text_style(pdf, item)):
indent = (item.level * self.level_indent) + pdf.l_margin
pdf.set_x(indent)
indent = item.level * self.level_indent
pdf.set_x(pdf.l_margin + indent)
pdf.multi_cell(
w=pdf.w - indent - pdf.r_margin,
w=pdf.epw - indent,
text=item.name,
new_x=XPos.END,
new_y=YPos.LAST,
Expand Down
Binary file modified test/html/html_title_with_render_title_tag.pdf
Binary file not shown.
35 changes: 28 additions & 7 deletions test/outline/test_outline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from fpdf import FPDF, TextStyle, TitleStyle, errors
from fpdf.enums import Align
from fpdf.outline import TableOfContents

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


def test_start_section_horizontal_alignment(tmp_path): # issue-1282
Lucas-C marked this conversation as resolved.
Show resolved Hide resolved
pdf = FPDF()
pdf.add_page()
pdf.set_font("Helvetica", "", 20)

# left align
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.L)
pdf.set_section_title_styles(level0)
pdf.start_section("left aligned section")

# center align
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.C)
pdf.set_section_title_styles(level0)
pdf.start_section("center aligned section")

# right align
level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.R)
pdf.set_section_title_styles(level0)
pdf.start_section("right aligned section")

assert_pdf_equal(
pdf, HERE / "test_start_section_horizontal_alignment.pdf", tmp_path
)


def test_set_section_title_styles_with_invalid_arg_type():
pdf = FPDF()
with pytest.raises(TypeError):
Expand Down Expand Up @@ -522,7 +548,6 @@ def footer():
pdf.cell(text=pdf.get_page_label(), center=True)

for test_number in range(3):

pdf = FPDF()
pdf.footer = footer

Expand Down Expand Up @@ -584,13 +609,10 @@ def footer():
pdf.ln()
pdf.add_font(
family="Quicksand",
style="",
fname=HERE.parent / "fonts" / "Quicksand-Regular.otf",
)
toc = TableOfContents()
toc.text_style = TextStyle(
font_family="Quicksand", font_style="", font_size_pt=14
)
toc.text_style = TextStyle(font_family="Quicksand", font_size_pt=14)
pdf.insert_toc_placeholder(toc.render_toc, allow_extra_pages=True)

if test_number == 2:
Expand All @@ -605,8 +627,7 @@ def footer():
)
pdf.ln()
pdf.ln()
toc = TableOfContents()
toc.use_section_title_styles = True
toc = TableOfContents(use_section_title_styles=True)
pdf.insert_toc_placeholder(toc.render_toc, allow_extra_pages=True)

pdf.set_page_label(label_style="D")
Expand Down
Binary file not shown.
Binary file modified test/outline/toc_with_extra_page_0.pdf
Binary file not shown.
Binary file modified test/outline/toc_with_extra_page_1.pdf
Binary file not shown.
Binary file modified test/outline/toc_with_extra_page_2.pdf
Binary file not shown.
Loading