From 0981d5aa58d3fdba2f4c3f6e05453e23333931be Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Tue, 9 Apr 2024 09:32:36 +0200 Subject: [PATCH] FPDF.table() now raises an error when a single row is too high to be rendered on a single page --- CHANGELOG.md | 1 + fpdf/fpdf.py | 2 +- fpdf/table.py | 32 ++++++++++++------ test/table/table_with_rowspan_and_pgbreak.pdf | Bin 5655 -> 5653 bytes test/table/test_table.py | 27 +++++++++++++++ test/table/test_table_rowspan.py | 5 ++- 6 files changed, 53 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fcceeae6..6194d115c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default', * fixed type hint of member `level` in class [`OutlineSection`](https://py-pdf.github.io/fpdf2/fpdf/outline.html#fpdf.outline.OutlineSection) from `str` to `int`. ### Changed * improved the performance of `FPDF.start_section()` - _cf._ [issue #1092](https://github.com/py-pdf/fpdf2/issues/1092) +* [`FPDF.table()`](https://py-pdf.github.io/fpdf2/Tables.html) now raises an error when a single row is too high to be rendered on a single page ### Deprecated * The `dd_tag_indent` & `li_tag_indent` parameters of `FPDF.write_html()` are replaced by the new `tag_indents` generic parameter. * The `heading_sizes` & `pre_code_font` parameters of `FPDF.write_html()` are replaced by the new `tag_styles` generic parameter. diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index ec75d94d5..7ce239833 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -3402,7 +3402,7 @@ def will_page_break(self, height): """ return ( # ensure that there is already some content on the page: - self.y > self.t_margin + self.y >= self.t_margin and self.y + height > self.page_break_trigger and not self.in_footer and self.accept_page_break diff --git a/fpdf/table.py b/fpdf/table.py index 75867342a..85c24aae7 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -185,7 +185,7 @@ def render(self): ) # Defining table global horizontal position: - prev_l_margin = self._fpdf.l_margin + prev_x, prev_y, prev_l_margin = self._fpdf.x, self._fpdf.y, self._fpdf.l_margin if table_align == Align.C: self._fpdf.l_margin = (self._fpdf.w - self._width) / 2 self._fpdf.x = self._fpdf.l_margin @@ -213,10 +213,21 @@ def render(self): # actually render the cells self._fpdf.y += self._outer_border_margin[1] for i, row in enumerate(self.rows): - # pylint: disable=protected-access - page_break = self._fpdf._perform_page_break_if_need_be( - row_info[i].pagebreak_height - ) + pagebreak_height = row_info[i].pagebreak_height + page_break = self._fpdf.will_page_break(pagebreak_height) + if page_break: + if self._fpdf.y + pagebreak_height > self._fpdf.page_break_trigger: + # Restoring original position on page: + self._fpdf.x, self._fpdf.y, self._fpdf.l_margin = ( + prev_x, + prev_y, + prev_l_margin, + ) + raise ValueError( + f"The row with index {i} is too high and cannot be rendered on a single page" + ) + # pylint: disable=protected-access + self._fpdf._perform_page_break_if_need_be(pagebreak_height) if page_break and i >= self._num_heading_rows: # repeat headings on top: self._fpdf.y += self._outer_border_margin[1] @@ -365,9 +376,8 @@ def _render_table_cell( # place cursor (required for images after images) - if ( - height_query_only - ): # not rendering, cell_x_positions is not relevant (and probably not provided) + # not rendering, cell_x_positions is not relevant (and probably not provided): + if height_query_only: cell_x = 0 else: cell_x = cell_x_positions[j] @@ -476,7 +486,7 @@ def _render_table_cell( dy = 0 if cell_height is not None: - actual_text_height = cell_height_info.rendered_height[j] + actual_text_height = cell_height_info.rendered_heights[j] if v_align == VAlign.M: dy = (cell_height - actual_text_height) / 2 @@ -841,8 +851,10 @@ def write(self, text, align=None): @dataclass(frozen=True) class RowLayoutInfo: height: float + # accumulated rowspans to take in account when considering page breaks: pagebreak_height: float - rendered_height: dict + # heights of every cell in the row: + rendered_heights: dict merged_heights: list diff --git a/test/table/table_with_rowspan_and_pgbreak.pdf b/test/table/table_with_rowspan_and_pgbreak.pdf index 837ed878969a7afd4aa82d675efc29c04f476743..1e0bba2e6d8d9f9e5cd7c7598b992dd5e51ca6a0 100644 GIT binary patch delta 1399 zcmbQPGgW6p3}d}9mz^C~aY<2XVlG$3oVDS;lkb=a>^;B!LSa$IgsAw%$zo-qklITWkDqx8&N zp6|2f-rvZObbP+u-@@|BA76en&Hc|M@GZ@DwWDj)gM018HBXqX6@TFQVyBmIn3?n0 zp2aEqs(<``=&f)#&USuZm;h&w=i+9w4HwfE7H^K9+jB0jwB^CSBXfeMoI79cKfV3O zx*0Wg@*G^zwkO;*59@CEd4SKZe)GhwJtb=%bCqp2eE9TKLCf-#x@tSt{AG3$3l$C) zEcN2s|9<;P0Zx9wun9LVS}izIbAW-Ydw;LUI1h zFS1lGsYoj>Tsz0=YnJL3{`bLrW*Hep>%VIoo@dMc8+D6u+CNjKFC|Y~juiLX+r-ts zn=kj??!m>CTbHh$FyC*&!?2tfkLG_B)!&~^dT?Xvwbi2gYrN9_ob;U>xy-X-ivDGj zxL+${<$PvHPntGeKe6%r+zTt$G3*GfES>t`8h?NCC$Og4Yn#m>ExT3M z8y8ne#)$XdZ+!eX{llk^n!CQ_{&3fLcWGZqP4)h-x$O48r#%q;s4KOe*Zpy7L8#So zUiarIAC6iUUzdJAD^~x})K=FWx9hWG^SAz-zizwR*IoOX-M@zIxmQxlKCj39;p}z! zhbR9Ki7Tt+@7q_u^n>s9*RQH9PQQ0$NvjC<;NQBQ^=GOEvdw$zz|lWci1nUwk&6{tvsq%;!JguT{UK>|n}`{JNk2 z418BU5Pf~~c)^;^rO!|EKiu1gTW7cxc}>L5vT= z7S|ROU%S8dh>i4~SncbJ_VHx*zjCczS=wqH`)jFZ@xEjc!gzB3w_ba_KXVa^v&#tWOn;jgsHzI2DqpPkb9PZWH zz6$;vGWp+=hOkTdizk*r*A}g--@fw6=Ep delta 1429 zcmbQLGhJsx3}d}1mz^C~aY<2XVlG$3oVDS;`Bw}C_P$^Lq0sWmjmPVMcq~*9VCT|i zJkB+3)ru`yyP4|j{EYl^j+>p`bv*Z%jsE$3wa*QEZX2xP`MKHh?!@QoEgw2aD({wm zlCyp1ZHBik)yFv0dunPP|GRD*GAqlwPB2xpm9IW-vj6Pj?{`Xq>L;A$EeX*`+Oo(? zNKfTz*NJuR_urpfY5k!{ha>PHXPx_<;KM7-j+Asod6wOmZT8+&TmSp*qk_`v&pmJI ztT-&^pMUgo^A5JGwbsYK#W+NBO>2lb&bp;q(XGc+ab|e<{`GP)lAj)ZELqYp&tbO7 zrA%h-Ts5oZudjB#SQFN6m}Om`)Wh+zX2%Zun6KroKOX;V2#z@IX6K}_YT+67un&^G zI+gzJ(pm|o(oglK?^e$7cy{`+<_@#hzZbMJvAec8+_EIheA0#@2^V+mhw72soGX7`Sm~~L` z@3%LP9zJ+@W9qFgN&8wax0>{}lV|>_-1#JI-@Cj{YdZVWXKehZKMT)zkgOiEiko4T zYfNm#j_(CKcNNTk^Uwa!t4oJ3)=qsDG_%gWvwqcT&c1z`;oa%%?6gR0 zZ?wGs_w?Vh59{_-iP#tUpOHU$eeU+BVrd@(_j1Wx?S5cX`Ik#3-o9!1dih1c_jmm- zINE!E`nLJsz3+#8Se(zihoh$ZnD*g|6@oEK&S@ULXen4zbneF2Q?rt;^G9!L^W3p{ z|IT&SgZ?gmUB6Xm>$>$10{2Sli^s+{RzGh2;Cg-f!?-^J^U7*@`}XlZ>|Qth=GTHL z_czH&JPcK`eX>t1@jl!3>0j1{?=NCmWiP*Q+AE;?zXJOL>RZz9$M0}pGdrfgCtmOU zUim|pS4KAe-Y9rz@e)d)?F!?wbF-z zUxz)2`z0{X3+RrQ&OcOY6;C(I%u40@{KIkE@p(=YzJ>s<2boub9=iN(*tm)>!UYh0X~{=?|_^o#e36s>xV zQnzgP>6oYUnV)Ordi%{YSy}n(jTH<)Kp{_o3(PPuG&MFs7c(-lFvJiuHZwvOGd8fa zzz{REFhCbGF|ag)h{Yi+HZe4}#87CCX`qP(Of!;ROG^yBrUsUlldDB-V@;h*j9ttv zoXsrV42%uTEuCFl-Ao(}ogGbF9GxtT4ee|Qs)&UYxrs$36-B9OT&9L*hFq$uuKsRZ E0EDr0#{d8T diff --git a/test/table/test_table.py b/test/table/test_table.py index ed51cc16a..f44027dd4 100644 --- a/test/table/test_table.py +++ b/test/table/test_table.py @@ -775,3 +775,30 @@ def test_table_cell_fill_mode(tmp_path): pass pdf.ln() assert_pdf_equal(pdf, HERE / "table_cell_fill_mode.pdf", tmp_path) + + +def test_table_with_very_long_text(): + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica") + with pytest.raises(ValueError) as error: + with pdf.table() as table: + row = table.row() + row.cell(LOREM_IPSUM) + # Adding columns to have the content of the 1st cell to overflow: + row.cell("") + row.cell("") + assert ( + str(error.value) + == "The row with index 0 is too high and cannot be rendered on a single page" + ) + with pytest.raises(ValueError) as error: + with pdf.table() as table: + row = table.row() + row.cell("") + row.cell("") + row.cell(LOREM_IPSUM) + assert ( + str(error.value) + == "The row with index 0 is too high and cannot be rendered on a single page" + ) diff --git a/test/table/test_table_rowspan.py b/test/table/test_table_rowspan.py index 47a81f560..25afc65ed 100644 --- a/test/table/test_table_rowspan.py +++ b/test/table/test_table_rowspan.py @@ -168,9 +168,6 @@ def test_table_with_rowspan_and_pgbreak(tmp_path): ] pdf.add_page() - y0 = pdf.h - pdf.b_margin - with pdf.local_context(**line_opts): - pdf.line(0, y0, pdf.w, y0) # simple table # with pdf.table(TABLE_DATA, **table_opts): @@ -181,7 +178,9 @@ def test_table_with_rowspan_and_pgbreak(tmp_path): # -- verify break occurs before the offending rowspan # -- verify header reproduction pdf.set_y(pdf.h - 85) + y0 = pdf.h - pdf.b_margin with pdf.local_context(**line_opts): + pdf.line(0, y0, pdf.w, y0) pdf.line(0, pdf.y, pdf.w, pdf.y) with pdf.table(TABLE_DATA, **table_opts): pass