From 1b7c3dfa019570641f351fd1c73d0fde975547da Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Thu, 7 Nov 2024 21:07:38 +0530 Subject: [PATCH 01/11] add horizontal centering in TextStyle --- fpdf/fonts.py | 14 ++++++++--- fpdf/fpdf.py | 11 ++++++++- test/outline/test_outline.py | 22 ++++++++++++++++++ ...est_start_section_horizontal_alignment.pdf | Bin 0 -> 2442 bytes 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 test/outline/test_start_section_horizontal_alignment.pdf diff --git a/fpdf/fonts.py b/fpdf/fonts.py index d7ff1194a..5a042b370 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -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 @@ -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: Optional[int] | Optional[Align] | Optional[str] = None, b_margin: Optional[int] = None, ): super().__init__( @@ -136,7 +136,15 @@ def __init__( fill_color, ) self.t_margin = t_margin or 0 - self.l_margin = l_margin or 0 + + # added support for 'Align' and 'str' type values for l_margin + if isinstance(l_margin, int) or isinstance(l_margin, Align): + self.l_margin = l_margin + elif isinstance(l_margin, str): + self.l_margin = Align.coerce(l_margin) + else: + self.l_margin = 0 + self.b_margin = b_margin or 0 def __repr__(self): diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 88389c8c5..07d30cbac 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -5234,12 +5234,20 @@ def start_section(self, name, level=0, strict=True): with self._marked_sequence(title=name) as struct_elem: outline_struct_elem = struct_elem with self.use_text_style(text_style): + + # check if l_margin value is of type Align or string + align = Align.L + if isinstance(text_style.l_margin, Align) or isinstance(text_style.l_margin, str): + align = text_style.l_margin + self.multi_cell( w=self.epw, h=self.font_size, text=name, + align=align, new_x=XPos.LMARGIN, new_y=YPos.NEXT, + center=True if title_style.l_margin == Align.C else False, ) self._outline.append( OutlineSection(name, level, self.page, dest, outline_struct_elem) @@ -5251,7 +5259,8 @@ def use_text_style(self, text_style: TextStyle): 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, int): + self.set_x(text_style.l_margin) with self.use_font_face(text_style): yield if text_style and text_style.b_margin: diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index d864124d4..c5ef3a5e3 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -66,6 +66,28 @@ def test_incoherent_start_section_hierarchy(): with pytest.raises(ValueError): pdf.start_section("Subtitle", level=2) +def test_start_section_horizontal_alignment(tmp_path): # issue-1282 + + pdf = FPDF() + pdf.add_page() + + # 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() diff --git a/test/outline/test_start_section_horizontal_alignment.pdf b/test/outline/test_start_section_horizontal_alignment.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cda4bdfea016732e68e47e5408227bcd30386d29 GIT binary patch literal 2442 zcmb7GU5Fc16jsE)L5m=WcCB2hOSj7I%$>|6lM)t_Or~ACZDJB>-LTT>zF1JCf-mZW#RmmJeJKcv4m)(6BZ zTh#HrG#wHRhaN&+bp_jOTuu7@fE}tfLpX3 z5YqI0iV8_Ip;H0QDlmTzn?@{8>5Gl!>gMl9n80+M6@>Oghu^pIRI4}d^S*u8_ zLPRGkjzyXWE3B#pdjgG2P?{w4qrieH@<|{em>dd`#J7+m#yDie49BG;fZn(!B|5SE zE)m3e;@NZwVO3$bLK+aWFAU#V`}U&n!lU=TeCfimQ*WI=^H1;l*_%&YsyEQbZ%^E| z=i*&AP5k!v^5geRzx3k+PYt!*&%UOA{mJT?=YM{9YWmd^3vYb+`mGP0K6iQX#>ZOS z+WH5|g|lD%^2mF82jA?yY+zb=2e^dP-qm+k-l*>@jZ`QuM|d>2zjN;1`m%2ik~Lt^;QH1QB| zR|s`1QxIz&RO)y(5|24v*$bU407H&W(pY$GG@#gILJWaxA?x=>3mG_!(!hx2ZN_s7 ze5*w$>WHunFjX>y{BwPfawUDQrtfo$94u2*274HFGzJG^O$N$zhl(tek=m+94u@Uj za4_4hrhpUKV75UqY^GqQGT(~&Ag!&?HJ9``Se874JsS04ktNi*DmY(Md?339RY|NO z6xRkHY?Lk_kT4uO3P68tH06>WMW*Z69_(~T;ziznO>%K{TpGNSG!~Q$^1r@ByaPc! z@=g*3j=glf1#LADW}rC%dK83U0I846o9s)v{fSaDXXEA;pUzs7STT5mPT+20#YjB< z$ zl};ldD;q80nWa9NGTUz095;TiuhpnRf#-HrI%(f{I`QIxh?7QbBMFWTZwFN{VfJgL zgHNL={eUdV3dT|aD~hTr3s~8wsQdPFLmvjDCrAiug0%5Mc~#A+sE0OVu+!{?yv!!W@Ye6d1!uOoQL~(O3~!jKwtQ!PdK28pp%Xg|Tz8GFBVT zsUVpVJVhSQq2|Z<;etG#L(6l$qJinSB#8g;w47%M>%AENQ%DCN|5d?ks$k)BI_i`P y#d29Q3TnQn8d9Y!8G22xmK8&)=JT4O=Vt%^L~OMoH3J$?92aFxn4GND4dFkD1ga|l literal 0 HcmV?d00001 From c451cb238a6d1b8914ce4e951b598e3ed847606f Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Thu, 7 Nov 2024 21:32:55 +0530 Subject: [PATCH 02/11] update testcase for TextStyle horizontal centering --- fpdf/fpdf.py | 14 +++++++------- test/outline/test_outline.py | 4 ++-- ...est_start_section_horizontal_alignment.pdf | Bin 2442 -> 2432 bytes 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 07d30cbac..01d08038b 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -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) or isinstance(text_style.l_margin, str): + align = text_style.l_margin page_break_triggered = self.multi_cell( w=self.epw, h=self.font_size, @@ -5220,9 +5224,10 @@ 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) else 0, bottom=text_style.b_margin or 0, ), ) @@ -5235,11 +5240,6 @@ def start_section(self, name, level=0, strict=True): outline_struct_elem = struct_elem with self.use_text_style(text_style): - # check if l_margin value is of type Align or string - align = Align.L - if isinstance(text_style.l_margin, Align) or isinstance(text_style.l_margin, str): - align = text_style.l_margin - self.multi_cell( w=self.epw, h=self.font_size, @@ -5247,7 +5247,7 @@ def start_section(self, name, level=0, strict=True): align=align, new_x=XPos.LMARGIN, new_y=YPos.NEXT, - center=True if title_style.l_margin == Align.C else False, + center=True if text_style.l_margin == Align.C else False, ) self._outline.append( OutlineSection(name, level, self.page, dest, outline_struct_elem) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index c5ef3a5e3..3da8e6367 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -70,6 +70,7 @@ def test_start_section_horizontal_alignment(tmp_path): # issue-1282 pdf = FPDF() pdf.add_page() + pdf.set_font("Helvetica", "", 20) # left align level0 = TextStyle("Helvetica", "", 20, (0, 0, 0), l_margin=Align.L) @@ -86,8 +87,7 @@ def test_start_section_horizontal_alignment(tmp_path): # issue-1282 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) - + assert_pdf_equal(pdf, HERE / "test_start_section_horizontal_alignment.pdf", tmp_path, generate=True) def test_set_section_title_styles_with_invalid_arg_type(): pdf = FPDF() diff --git a/test/outline/test_start_section_horizontal_alignment.pdf b/test/outline/test_start_section_horizontal_alignment.pdf index cda4bdfea016732e68e47e5408227bcd30386d29..7ab3e3e0c8dddd0ba921c684813d6a05de771288 100644 GIT binary patch delta 550 zcmeAYZV=vZnz7!*gv-v3tGJ{nH8Gc~VovY5vs_IMB5e=<#vI+g=Il)4#brE4TJ0Sh zFC^tDl$bd4_`LXbxOZpQH+}x~M_mnFo3@{l`Ev4rk>TO38gnm&t8}jWRLtlXoZ`AK zq)}1Wxz8z(<9dSmkFsOAFLqxvulQN@ZvJDrcN+y#r{+t3PSr7%V`Z9nb>&}+-!J`7 z=CwUyz9}HI_R^K|@AJLo=egKl*<8f*l|{tR(#+D($k@=((f|me3@z0S3?^5zH`g1R zD1bx^6!H|fzzhQeb7MmcF$+TrOfgFX3^7Y{b96C715EP_4WahdBUub|pCyK7V-sTx zF%v^fOH55M9B626h~YUy3sW#DcSQwidIJvo+8kw0{SQ;9bJDR&X nIXN4+Il3BH+Sw3P5erEIiA5z9MX70AhL(mFT&k+B{%%|V^uC}_ delta 504 zcmZn=?h@W`nz7#0gv-v3tGJ{nH8Gc~V$R(2XStdjBw8Qt4KJLwb>`2sD|W1f5B&oa zw#<;`kUcBFEdAnJcA%JU%PqOM3Ja;~ewPzFDrYv{(bCZENQjzpY%;q_=bBHkj9$Sh zuK!k<>|K5QiS|OJf-_1y2~um=Zd=K;?~~4LUcEN9Uh=xZ^Ys53yi-H}b*^;v5$2qK z&?bAU?A*EkzJK|&GefzAfiFG()$YsdzrXuX62Z4QgXt@ah>?MjiJ_r^xuJ=Psi{en zp^3VQ>EuH8W?K^z1p^RJ$W!0~GYkyOO$;%_EQ~BL#4HUa8*&KST9~8D7#d)jU Date: Thu, 7 Nov 2024 21:54:18 +0530 Subject: [PATCH 03/11] updated CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 770633f4f..e9e66a348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) From 6f243cbaa36a26e8410a587502d4e86ba04e2d8f Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Fri, 8 Nov 2024 08:34:00 +0530 Subject: [PATCH 04/11] remove generate=True in test case --- test/outline/test_outline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index 3da8e6367..c852270cf 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -87,7 +87,7 @@ def test_start_section_horizontal_alignment(tmp_path): # issue-1282 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, generate=True) + 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() From cd5234bdbdbd9a181a46575bf301cc6c8a6dc8bf Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Fri, 8 Nov 2024 09:04:03 +0530 Subject: [PATCH 05/11] replaced pipe operator with Union --- fpdf/fonts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 5a042b370..6b18310d2 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -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] | Optional[Align] | Optional[str] = None, + l_margin: Union[Optional[int], Optional[Align], Optional[str]] = None, b_margin: Optional[int] = None, ): super().__init__( From 79bfc4df1cc129bfbcbe217cc60777fa847bb30c Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Fri, 8 Nov 2024 09:35:50 +0530 Subject: [PATCH 06/11] remove linting errors --- fpdf/fonts.py | 2 +- fpdf/fpdf.py | 4 ++-- test/outline/test_outline.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 6b18310d2..7f5a86ba6 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -138,7 +138,7 @@ def __init__( self.t_margin = t_margin or 0 # added support for 'Align' and 'str' type values for l_margin - if isinstance(l_margin, int) or isinstance(l_margin, Align): + if isinstance(l_margin, (Align, int)): self.l_margin = l_margin elif isinstance(l_margin, str): self.l_margin = Align.coerce(l_margin) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 01d08038b..d63612c3f 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -5214,7 +5214,7 @@ def start_section(self, name, level=0, strict=True): 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) or isinstance(text_style.l_margin, str): + if isinstance(text_style.l_margin, (Align, str)): align = text_style.l_margin page_break_triggered = self.multi_cell( w=self.epw, @@ -5247,7 +5247,7 @@ def start_section(self, name, level=0, strict=True): align=align, new_x=XPos.LMARGIN, new_y=YPos.NEXT, - center=True if text_style.l_margin == Align.C else False, + center=text_style.l_margin == Align.C, ) self._outline.append( OutlineSection(name, level, self.page, dest, outline_struct_elem) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index c852270cf..3eefc0bbc 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -76,7 +76,7 @@ def test_start_section_horizontal_alignment(tmp_path): # issue-1282 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) From 7f702512a1d0bf89b702b9534602cbff319a5787 Mon Sep 17 00:00:00 2001 From: visheshdvivedi Date: Sat, 9 Nov 2024 09:54:01 +0530 Subject: [PATCH 07/11] formatted code using black --- fpdf/fpdf.py | 6 +++++- test/outline/test_outline.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index d63612c3f..3e2ed19dd 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -5227,7 +5227,11 @@ def start_section(self, name, level=0, strict=True): align=align, padding=Padding( top=text_style.t_margin or 0, - left=text_style.l_margin if isinstance(text_style.l_margin, int) else 0, + left=( + text_style.l_margin + if isinstance(text_style.l_margin, int) + else 0 + ), bottom=text_style.b_margin or 0, ), ) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index 3eefc0bbc..b62515d04 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -66,6 +66,7 @@ def test_incoherent_start_section_hierarchy(): with pytest.raises(ValueError): pdf.start_section("Subtitle", level=2) + def test_start_section_horizontal_alignment(tmp_path): # issue-1282 pdf = FPDF() @@ -87,7 +88,10 @@ def test_start_section_horizontal_alignment(tmp_path): # issue-1282 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) + 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() From c1b54098c3aa36a046821efd6377d887f837a32a Mon Sep 17 00:00:00 2001 From: Anderson Herzogenrath da Costa Date: Fri, 13 Dec 2024 17:24:33 -0500 Subject: [PATCH 08/11] l_margin can be float --- fpdf/fonts.py | 2 +- fpdf/fpdf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fpdf/fonts.py b/fpdf/fonts.py index 7f5a86ba6..d089b97f8 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -138,7 +138,7 @@ def __init__( self.t_margin = t_margin or 0 # added support for 'Align' and 'str' type values for l_margin - if isinstance(l_margin, (Align, int)): + if isinstance(l_margin, (Align, int, float)): self.l_margin = l_margin elif isinstance(l_margin, str): self.l_margin = Align.coerce(l_margin) diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index 3e2ed19dd..de72ac36d 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -5229,7 +5229,7 @@ def start_section(self, name, level=0, strict=True): top=text_style.t_margin or 0, left=( text_style.l_margin - if isinstance(text_style.l_margin, int) + if isinstance(text_style.l_margin, (int, float)) else 0 ), bottom=text_style.b_margin or 0, From f012bf9126026e87b3fef493656524a20c14227c Mon Sep 17 00:00:00 2001 From: Anderson Herzogenrath da Costa Date: Fri, 13 Dec 2024 17:39:27 -0500 Subject: [PATCH 09/11] redo test files --- test/outline/test_outline.py | 1 + ...est_start_section_horizontal_alignment.pdf | Bin 2432 -> 2426 bytes test/outline/toc_with_extra_page_0.pdf | Bin 66925 -> 66903 bytes test/outline/toc_with_extra_page_1.pdf | Bin 78020 -> 77999 bytes test/outline/toc_with_extra_page_2.pdf | Bin 67970 -> 67948 bytes 5 files changed, 1 insertion(+) diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index b62515d04..1366a4892 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -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 diff --git a/test/outline/test_start_section_horizontal_alignment.pdf b/test/outline/test_start_section_horizontal_alignment.pdf index 7ab3e3e0c8dddd0ba921c684813d6a05de771288..4a46ab414b4c24a5e69e2012e76ab988b0970a12 100644 GIT binary patch delta 105 zcmZn={w1^_hkbJq`zFR17bg>QR|{hoGZ#Y_S4%fDQwwKvQx_9w3nz0kOEYsj8y7+< eV!7<>xQa^>i%KerQq#B$Ee(yhR8?L5-M9ekOB+1^ delta 112 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 delta 5124 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 U0=>M22^1wr;d%4SmYNIy59KSRjQ{`u diff --git a/test/outline/toc_with_extra_page_1.pdf b/test/outline/toc_with_extra_page_1.pdf index a4132c19e0ef390e8f503ccb884b32eb2f4986c1..f3e2e19b673f3a2ed53f7a10a6aea2f37d6e177e 100644 GIT binary patch delta 5272 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 Ee<5bXzW@LL delta 5278 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 M%+b^|voYuV500D53IG5A diff --git a/test/outline/toc_with_extra_page_2.pdf b/test/outline/toc_with_extra_page_2.pdf index 730df25b80317788507fab00a2851babae27a4d2..edd7fc4088ac9b71b8a3f23101120f8e2d46c778 100644 GIT binary patch delta 5101 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 delta 5136 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 From ccf08b593c7e1bd2685b027fefd7c3b909259810 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:14:23 +0100 Subject: [PATCH 10/11] Minor improvements --- fpdf/enums.py | 14 ++++++++++---- fpdf/fonts.py | 5 ++--- fpdf/fpdf.py | 18 ++++++++++++++---- fpdf/outline.py | 29 ++++++++++++++++++++--------- test/outline/test_outline.py | 10 ++-------- 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/fpdf/enums.py b/fpdf/enums.py index 9eea8419e..65b548dbf 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -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) @@ -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) @@ -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 @@ -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": diff --git a/fpdf/fonts.py b/fpdf/fonts.py index d089b97f8..9618c7372 100644 --- a/fpdf/fonts.py +++ b/fpdf/fonts.py @@ -137,10 +137,9 @@ def __init__( ) self.t_margin = t_margin or 0 - # added support for 'Align' and 'str' type values for l_margin - if isinstance(l_margin, (Align, int, float)): + if isinstance(l_margin, (int, float)): self.l_margin = l_margin - elif isinstance(l_margin, str): + elif l_margin: self.l_margin = Align.coerce(l_margin) else: self.l_margin = 0 diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index de72ac36d..6ebe1e756 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -5215,7 +5215,7 @@ def start_section(self, name, level=0, strict=True): # check if l_margin value is of type Align or string align = Align.L if isinstance(text_style.l_margin, (Align, str)): - align = text_style.l_margin + align = Align.coerce(text_style.l_margin) page_break_triggered = self.multi_cell( w=self.epw, h=self.font_size, @@ -5243,7 +5243,6 @@ def start_section(self, name, level=0, strict=True): with self._marked_sequence(title=name) as struct_elem: outline_struct_elem = struct_elem with self.use_text_style(text_style): - self.multi_cell( w=self.epw, h=self.font_size, @@ -5259,16 +5258,27 @@ def start_section(self, name, level=0, strict=True): @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: - if isinstance(text_style.l_margin, int): - 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): diff --git a/fpdf/outline.py b/fpdf/outline.py index 3d2ede585..8b3eed2cb 100644 --- a/fpdf/outline.py +++ b/fpdf/outline.py @@ -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): @@ -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, diff --git a/test/outline/test_outline.py b/test/outline/test_outline.py index 1366a4892..ccd90bb13 100644 --- a/test/outline/test_outline.py +++ b/test/outline/test_outline.py @@ -69,7 +69,6 @@ def test_incoherent_start_section_hierarchy(): def test_start_section_horizontal_alignment(tmp_path): # issue-1282 - pdf = FPDF() pdf.add_page() pdf.set_font("Helvetica", "", 20) @@ -549,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 @@ -611,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: @@ -632,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") From bc67e83191cd617889cfbb92c0668f14e9b311ef Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:40:52 +0100 Subject: [PATCH 11/11] Supporting an Align value as l_margin in HTML tags styles --- fpdf/html.py | 15 ++++++++++++--- .../html/html_title_with_render_title_tag.pdf | Bin 1016 -> 1017 bytes 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/fpdf/html.py b/fpdf/html.py index 6e3546514..a568aa8ad 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -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 @@ -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, @@ -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: diff --git a/test/html/html_title_with_render_title_tag.pdf b/test/html/html_title_with_render_title_tag.pdf index fe264641d351942006ec265c2c364f9f96026a6a..808926a41d52308bda6b5b5aa15b6739f37d3179 100644 GIT binary patch delta 210 zcmeyt{*!%!10$o+WJg9LQ^|-Ik~1`CL`S@uIZLBuNyAl1X@{$sJD#Xdf41z|RE;Sq z)1EBJ(rQUz{Hn!L#9GA2a5YgZc=K|`iHw>?3I-sckf*=}W*8Wlniyh;nHgD3PGq); zHa0UdaW=3pc6ByzwKQ@ywJ?jPt>PBTlQ?K#*~z4 zPnKk9wWKg^)nX}PEn;Liq$;Mcc?IJ{MomKn0}xQiQ{VzK3=B+73^2sZjLat|G229& z85zTJ35=2IGMN_8JIa5fVeIuX0GNYb~c8DRK#-G*>M$@Bo>ua6s4wdnH!jK Lsj9mAyKw;kHQ+m3