diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 11aa083f4..70324c7cf 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -24,11 +24,11 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install system dependencies ⚙️ if: matrix.platform == 'ubuntu-latest' - run: sudo apt-get install ghostscript libjpeg-dev + run: sudo apt-get update && sudo apt-get install ghostscript libjpeg-dev - name: Install qpdf ⚙️ if: matrix.platform == 'ubuntu-latest' && matrix.python-version != '3.9' # We run the unit tests WITHOUT qpdf for a single parallel execution / Python version: - run: sudo apt-get install qpdf + run: sudo apt-get update && sudo apt-get install qpdf - name: Install Python dependencies ⚙️ run: | python -m pip install --upgrade pip setuptools wheel diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c984251..10cd073f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,15 @@ This can also be enabled programmatically with `warnings.simplefilter('default', ## [2.7.4] - Not released yet ### Added - documentation on how to embed `graphs` and `charts` generated using `Pygal` lib: [documentation section](https://pyfpdf.github.io/fpdf2/Maths.html#using-pygal) - thanks to @ssavi-ict -- Documentation on how to use `fpdf2` with [FastAPI](https://fastapi.tiangolo.com/): - thanks to @KamarulAdha +- documentation on how to use `fpdf2` with [FastAPI](https://fastapi.tiangolo.com/): - thanks to @KamarulAdha +- [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): `` elements can now be aligned left or right on the page using `align=` +### Fixed +- [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): text overflow in the last cell of the header row is now properly handled +- [`FPDF.table()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.table): when `align="RIGHT"` is provided, the page right margin is now properly taken in consideration ### Changed - [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html) does not render the top row as a header, in bold with a line below, when no `
` are used, in order to be more backward-compatible with earlier versions of `fpdf2` - _cf._ [#740](https://github.com/PyFPDF/fpdf2/issues/740) +### Deprecated +- the `split_only` optional parameter of [`FPDF.multi_cell()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell), which is replaced by two new distincts optional parameters: `dry_run` & `output` ## [2.7.3] - 2023-04-03 ### Fixed diff --git a/docs/Development.md b/docs/Development.md index 7c2c90eef..1ce382398 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -198,7 +198,7 @@ To preview the API documentation, launch a local rendering server with: ## PDF spec & new features The **PDF 1.7 spec** is available on Adobe website: -[PDF32000_2008.pdf](https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf). +[PDF32000_2008.pdf](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf). It may be intimidating at first, but while technical, it is usually quite clear and understandable. diff --git a/docs/HTML.md b/docs/HTML.md index 20189a1e0..f854d06a1 100644 --- a/docs/HTML.md +++ b/docs/HTML.md @@ -84,7 +84,7 @@ pdf.output("html.pdf") * `
    `, `
      `, `
    • `: ordered, unordered and list items (can be nested) * `
      `, `
      `, `
      `: description list, title, details (can be nested) * ``, ``: superscript and subscript text -* ``: (and `border`, `width` attributes) +* `
      `: (with `align`, `border`, `width` attributes) + ``: optional tag, wraps the table header row + ``: optional tag, wraps the table footer row + ``: optional tag, wraps the table rows with actual content diff --git a/docs/LineBreaks.md b/docs/LineBreaks.md index f0304a798..a83deadb4 100644 --- a/docs/LineBreaks.md +++ b/docs/LineBreaks.md @@ -10,5 +10,3 @@ An automatic break is performed at the location of the nearest space or soft-hyp A soft-hyphen will be replaced by a normal hyphen when triggering a line break, and ignored otherwise. If the parameter `print_sh=False` in `multi_cell()` or `write()` is set to `True`, then they will print the soft-hyphen character to the document (as a normal hyphen with most fonts) instead of using it as a line break opportunity. - -When using [multi_cell()](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell), the parameter `split_only=True` will perform word-wrapping only and return the resulting multi-lines as a list of strings. This can be used in conjunction with the cursor position and document height to determine if inserting a [multi_cell()](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) will result in a page break. \ No newline at end of file diff --git a/docs/Text.md b/docs/Text.md index 8d1ee5a0f..45c737269 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -90,8 +90,7 @@ the background painted. Using `new_x="RIGHT", new_y="TOP", maximum height=pdf.font_size` can be useful to build tables with multiline text in cells. -In normal operation, returns a boolean indicating if page break was triggered. -When `split_only == True`, returns `txt` split into lines in an array (with any markdown markup removed). +In normal operation, returns a boolean indicating if page break was triggered. The return value can be altered by specifying the `output` parameter. [Signature and parameters for.multi_cell()](fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell) diff --git a/fpdf/enums.py b/fpdf/enums.py index 078c5391e..2c0e79197 100644 --- a/fpdf/enums.py +++ b/fpdf/enums.py @@ -129,6 +129,10 @@ def coerce(cls, value): return value if isinstance(value, str): + try: + return cls[value.upper()] + except KeyError: + pass try: flags = cls[value[0].upper()] for char in value[1:]: @@ -198,6 +202,7 @@ def coerce(cls, value): class TextEmphasis(CoerciveIntFlag): """ Indicates use of bold / italics / underline. + This enum values can be combined with & and | operators: style = B | I """ @@ -231,6 +236,24 @@ def coerce(cls, value): return super(cls, cls).coerce(value) +class MethodReturnValue(CoerciveIntFlag): + """ + Defines the return value(s) of a FPDF content-rendering method. + + This enum values can be combined with & and | operators: + PAGE_BREAK | LINES + """ + + PAGE_BREAK = 1 + "The method will return a boolean indicating if a page break occured" + + LINES = 2 + "The method will return a multi-lines array of strings, after performing word-wrapping" + + HEIGHT = 4 + "The method will return how much vertical space was used" + + class TableBordersLayout(CoerciveEnum): "Defines how to render table borders" diff --git a/fpdf/fpdf.py b/fpdf/fpdf.py index acdb42cd8..f0a2b9da8 100644 --- a/fpdf/fpdf.py +++ b/fpdf/fpdf.py @@ -60,6 +60,7 @@ class Image: EncryptionMethod, FontDescriptorFlags, FileAttachmentAnnotationName, + MethodReturnValue, PageLayout, PageMode, PathPaintRule, @@ -256,7 +257,7 @@ def check_page(fn): @wraps(fn) def wrapper(self, *args, **kwargs): - if not self.page and not kwargs.get("split_only"): + if not self.page and not (kwargs.get("dry_run") or kwargs.get("split_only")): raise FPDFException("No page open, you need to call add_page() first") return fn(self, *args, **kwargs) @@ -3342,6 +3343,19 @@ def _perform_page_break(self): def _has_next_page(self): return self.pages_count > self.page + @contextmanager + def _disable_writing(self): + self._out = lambda *args, **kwargs: None + self.add_page = lambda *args, **kwargs: None + self._perform_page_break = lambda *args, **kwargs: None + prev_x, prev_y = self.x, self.y + yield + # restore writing functions: + del self.add_page + del self._out + del self._perform_page_break + self.set_xy(prev_x, prev_y) # restore location + @check_page def multi_cell( self, @@ -3351,7 +3365,7 @@ def multi_cell( border=0, align=Align.J, fill=False, - split_only=False, + split_only=False, # DEPRECATED link="", ln="DEPRECATED", max_line_height=None, @@ -3360,6 +3374,8 @@ def multi_cell( new_x=XPos.RIGHT, new_y=YPos.NEXT, wrapmode: WrapMode = WrapMode.WORD, + dry_run=False, + output=MethodReturnValue.PAGE_BREAK, ): """ This method allows printing text with line breaks. They can be automatic @@ -3384,8 +3400,8 @@ def multi_cell( `C`: center; `X`: center around current x; `R`: right align fill (bool): Indicates if the cell background must be painted (`True`) or transparent (`False`). Default value: False. - split_only (bool): if `True`, does not output anything, only perform - word-wrapping and return the resulting multi-lines array of strings. + split_only (bool): **DEPRECATED since 2.7.4**: + Use `dry_run=True` and `output=("LINES",)` instead. link (str): optional link to add on the cell, internal (identifier returned by `add_link`) or external URL. new_x (fpdf.enums.XPos, str): New current position in x after the call. Default: RIGHT @@ -3398,13 +3414,46 @@ def multi_cell( character, instead of a line breaking opportunity. Default value: False wrapmode (fpdf.enums.WrapMode): "WORD" for word based line wrapping (default), "CHAR" for character based line wrapping. + dry_run (bool): if `True`, does not output anything in the document. + Can be useful when combined with `output`. + output (fpdf.enums.MethodReturnValue): defines what this method returns. + If several enum values are joined, the result will be a tuple. Using `new_x=XPos.RIGHT, new_y=XPos.TOP, maximum height=pdf.font_size` is useful to build tables with multiline text in cells. - Returns: a boolean indicating if page break was triggered, - or if `split_only == True`: `txt` splitted into lines in an array + Returns: a single value or a tuple, depending on the `output` parameter value """ + if split_only: + warnings.warn( + ( + 'The parameter "split_only" is deprecated.' + ' Use instead dry_run=True and output="LINES".' + ), + DeprecationWarning, + stacklevel=3, + ) + if dry_run or split_only: + with self._disable_writing(): + return self.multi_cell( + w=w, + h=h, + txt=txt, + border=border, + align=align, + fill=fill, + link=link, + ln=ln, + max_line_height=max_line_height, + markdown=markdown, + print_sh=print_sh, + new_x=new_x, + new_y=new_y, + wrapmode=wrapmode, + dry_run=False, + split_only=False, + output=MethodReturnValue.LINES if split_only else output, + ) wrapmode = WrapMode.coerce(wrapmode) if isinstance(w, str) or isinstance(h, str): raise ValueError( @@ -3443,10 +3492,6 @@ def multi_cell( align = Align.coerce(align) page_break_triggered = False - if split_only: - self._out = lambda *args, **kwargs: None - self.add_page = lambda *args, **kwargs: None - self._perform_page_break_if_need_be = lambda *args, **kwargs: None if h is None: h = self.font_size @@ -3462,6 +3507,7 @@ def multi_cell( prev_font_style, prev_underline = self.font_style, self.underline prev_x, prev_y = self.x, self.y + total_height = 0 if not border: border = "" @@ -3490,8 +3536,6 @@ def multi_cell( trailing_nl=False, ) ] - if align == Align.X: - prev_x = self.x should_render_bottom_blank_cell = False for text_line_index, text_line in enumerate(text_lines): is_last_line = text_line_index == len(text_lines) - 1 @@ -3527,6 +3571,7 @@ def multi_cell( link=link, ) page_break_triggered = page_break_triggered or new_page + total_height += current_cell_height if not is_last_line and align == Align.X: # prevent cumulative shift to the left self.x = prev_x @@ -3566,26 +3611,29 @@ def multi_cell( if new_y == YPos.TOP: # We may have jumped a few lines -> reset self.y = prev_y - if split_only: - # restore writing functions - del self.add_page - del self._out - del self._perform_page_break_if_need_be - self.set_xy(prev_x, prev_y) # restore location - result = [] - for text_line in text_lines: - characters = [] - for frag in text_line.fragments: - characters.extend(frag.characters) - result.append("".join(characters)) - return result if markdown: if self.font_style != prev_font_style: self.font_style = prev_font_style self.current_font = self.fonts[self.font_family + self.font_style] self.underline = prev_underline - return page_break_triggered + output = MethodReturnValue.coerce(output) + return_value = () + if output & MethodReturnValue.PAGE_BREAK: + return_value += (page_break_triggered,) + if output & MethodReturnValue.LINES: + output_lines = [] + for text_line in text_lines: + characters = [] + for frag in text_line.fragments: + characters.extend(frag.characters) + output_lines.append("".join(characters)) + return_value += (output_lines,) + if output & MethodReturnValue.HEIGHT: + return_value += (total_height,) + if len(return_value) == 1: + return return_value[0] + return return_value @check_page def write( diff --git a/fpdf/html.py b/fpdf/html.py index bf4eb4334..ceafd0eee 100644 --- a/fpdf/html.py +++ b/fpdf/html.py @@ -434,8 +434,10 @@ def handle_starttag(self, tag, attrs): if self.table_line_separators else "SINGLE_TOP_LINE" ) + align = attrs.get("align", "center").upper() self.table = Table( self.pdf, + align=align, borders_layout=borders_layout, line_height=self.h * 1.30, width=width, diff --git a/fpdf/table.py b/fpdf/table.py index ec132df88..96da9455b 100644 --- a/fpdf/table.py +++ b/fpdf/table.py @@ -1,8 +1,9 @@ from dataclasses import dataclass from numbers import Number -from typing import List, Optional, Union +from typing import Optional, Union from .enums import Align, TableBordersLayout, TableCellFillMode +from .enums import MethodReturnValue from .errors import FPDFException from .fonts import FontFace @@ -10,6 +11,12 @@ DEFAULT_HEADINGS_STYLE = FontFace(emphasis="BOLD") +@dataclass(frozen=True) +class RowLayoutInfo: + height: int + triggers_page_jump: bool + + class Table: """ Object that `fpdf.FPDF.table()` yields, used to build a table in the document. @@ -86,7 +93,9 @@ def render(self): ) table_align = Align.coerce(self._align) if table_align == Align.J: - raise ValueError("JUSTIFY is an invalid value for table .align") + raise ValueError( + "JUSTIFY is an invalid value for FPDF.table() 'align' parameter" + ) if self._first_row_as_headings: if not self._headings_style: raise ValueError( @@ -110,20 +119,19 @@ def render(self): self._fpdf.l_margin = (self._fpdf.w - self._width) / 2 self._fpdf.x = self._fpdf.l_margin elif table_align == Align.R: - self._fpdf.l_margin = self._fpdf.w - self._width + self._fpdf.l_margin = self._fpdf.w - self._fpdf.r_margin - self._width self._fpdf.x = self._fpdf.l_margin elif self._fpdf.x != self._fpdf.l_margin: self._fpdf.l_margin = self._fpdf.x # Starting the actual rows & cells rendering: for i in range(len(self.rows)): - with self._fpdf.offset_rendering() as test: - self._render_table_row(i) - if test.page_break_triggered: + row_layout_info = self._get_row_layout_info(i) + if row_layout_info.triggers_page_jump: # pylint: disable=protected-access self._fpdf._perform_page_break() if self._first_row_as_headings: # repeat headings on top: self._render_table_row(0) - self._render_table_row(i) + self._render_table_row(i, row_layout_info) # Restoring altered FPDF settings: self._fpdf.l_margin = prev_l_margin self._fpdf.x = self._fpdf.l_margin @@ -183,64 +191,43 @@ def get_cell_border(self, i, j): border.remove("B") return "".join(border) - def _render_table_row(self, i, fill=False, **kwargs): + def _render_table_row(self, i, row_layout_info=None, fill=False, **kwargs): + if not row_layout_info: + row_layout_info = self._get_row_layout_info(i) row = self.rows[i] - lines_heights_per_cell = self._get_lines_heights_per_cell(i) - row_height = ( - max(sum(lines_heights) for lines_heights in lines_heights_per_cell) - if lines_heights_per_cell - else 0 - ) j = 0 while j < len(row.cells): - cell_line_height = row_height / len(lines_heights_per_cell[j]) self._render_table_cell( i, j, - cell_line_height=cell_line_height, - row_height=row_height, + row_height=row_layout_info.height, fill=fill, **kwargs, ) j += row.cells[j].colspan - self._fpdf.ln(row_height) + self._fpdf.ln(row_layout_info.height) # pylint: disable=inconsistent-return-statements def _render_table_cell( self, i, j, - cell_line_height, row_height, fill=False, - lines_heights_only=False, **kwargs, ): - """ - If `lines_heights_only` is True, returns a list of lines (subcells) heights. - """ row = self.rows[i] cell = row.cells[j] col_width = self._get_col_width(i, j, cell.colspan) - lines_heights = [] if cell.img: - if lines_heights_only: - info = self._fpdf.preload_image(cell.img)[2] - img_ratio = info.width / info.height - if cell.img_fill_width or row_height * img_ratio > col_width: - img_height = col_width / img_ratio - else: - img_height = row_height - lines_heights += [img_height] - else: - x, y = self._fpdf.x, self._fpdf.y - self._fpdf.image( - cell.img, - w=col_width, - h=0 if cell.img_fill_width else row_height, - keep_aspect_ratio=True, - ) - self._fpdf.set_xy(x, y) + x, y = self._fpdf.x, self._fpdf.y + self._fpdf.image( + cell.img, + w=col_width, + h=0 if cell.img_fill_width else row_height, + keep_aspect_ratio=True, + ) + self._fpdf.set_xy(x, y) text_align = cell.align or self._text_align if not isinstance(text_align, (Align, str)): text_align = text_align[j] @@ -248,9 +235,6 @@ def _render_table_cell( style = self._headings_style else: style = cell.style or row.style - if lines_heights_only and style: - # Avoid to generate font-switching instructions: BT /F... Tf ET - style = style.replace(emphasis=None) if style and style.fill_color: fill = True elif ( @@ -271,24 +255,21 @@ def _render_table_cell( else FontFace(fill_color=self._cell_fill_color) ) with self._fpdf.use_font_face(style): - lines = self._fpdf.multi_cell( + page_break, height = self._fpdf.multi_cell( w=col_width, h=row_height, txt=cell.text, - max_line_height=cell_line_height, + max_line_height=self._line_height, border=self.get_cell_border(i, j), align=text_align, new_x="RIGHT", new_y="TOP", fill=fill, - split_only=lines_heights_only, markdown=self._markdown, + output=MethodReturnValue.PAGE_BREAK | MethodReturnValue.HEIGHT, **kwargs, ) - if lines_heights_only and not cell.img: - lines_heights += (len(lines) or 1) * [self._line_height] - if lines_heights_only: - return lines_heights + return page_break, height def _get_col_width(self, i, j, colspan=1): if not self._col_widths: @@ -307,20 +288,28 @@ def _get_col_width(self, i, j, colspan=1): col_width += col_ratio * self._width return col_width - def _get_lines_heights_per_cell(self, i) -> List[List[int]]: + def _get_row_layout_info(self, i): + """ + Uses FPDF.offset_rendering() to detect a potential page jump + and compute the cells heights. + """ row = self.rows[i] - lines_heights = [] - for j in range(len(row.cells)): - lines_heights.append( - self._render_table_cell( + heights_per_cell = [] + any_page_break = False + # pylint: disable=protected-access + with self._fpdf._disable_writing(): + for j in range(len(row.cells)): + page_break, height = self._render_table_cell( i, j, - cell_line_height=self._line_height, row_height=self._line_height, - lines_heights_only=True, ) - ) - return lines_heights + any_page_break = any_page_break or page_break + heights_per_cell.append(height) + row_height = ( + max(height for height in heights_per_cell) if heights_per_cell else 0 + ) + return RowLayoutInfo(row_height, any_page_break) class Row: @@ -367,7 +356,7 @@ def cell( return cell -@dataclass +@dataclass(frozen=True) class Cell: "Internal representation of a table cell" __slots__ = ( # RAM usage optimization diff --git a/fpdf/template.py b/fpdf/template.py index c59392bd3..9e04a49d7 100644 --- a/fpdf/template.py +++ b/fpdf/template.py @@ -300,7 +300,8 @@ def split_multicell(self, text, element_name): h=element["y2"] - element["y1"], txt=str(text), align=element.get("align", ""), - split_only=True, + dry_run=True, + output="LINES", ) def _text( @@ -358,7 +359,12 @@ def _text( ) else: # trim to fit exactly the space defined text = pdf.multi_cell( - w=width, h=height, txt=text, align=align, split_only=True + w=width, + h=height, + txt=text, + align=align, + dry_run=True, + output="LINES", )[0] pdf.cell(w=width, h=height, txt=text, border=0, align=align, fill=fill) diff --git a/test/html/html_table_with_bgcolor.pdf b/test/html/html_table_with_bgcolor.pdf index 76d738ad7..b9da52327 100644 Binary files a/test/html/html_table_with_bgcolor.pdf and b/test/html/html_table_with_bgcolor.pdf differ diff --git a/test/html/html_table_with_imgs_captions_and_colspan.pdf b/test/html/html_table_with_imgs_captions_and_colspan.pdf index dc352bb9e..f27a78585 100644 Binary files a/test/html/html_table_with_imgs_captions_and_colspan.pdf and b/test/html/html_table_with_imgs_captions_and_colspan.pdf differ diff --git a/test/html/html_table_with_width_and_align.pdf b/test/html/html_table_with_width_and_align.pdf new file mode 100644 index 000000000..eba36ccea Binary files /dev/null and b/test/html/html_table_with_width_and_align.pdf differ diff --git a/test/html/test_html_table.py b/test/html/test_html_table.py index d9c1723fe..23cc66b31 100644 --- a/test/html/test_html_table.py +++ b/test/html/test_html_table.py @@ -242,6 +242,22 @@ def test_html_table_with_multiline_cells_and_split_over_page(tmp_path): ) +def test_html_table_with_width_and_align(tmp_path): + pdf = FPDF() + pdf.set_font_size(24) + pdf.add_page() + pdf.write_html( + """
      + + + + + +
      leftcenterright
      123
      456
      """ + ) + assert_pdf_equal(pdf, HERE / "html_table_with_width_and_align.pdf", tmp_path) + + def test_html_table_invalid(caplog): pdf = FPDF() pdf.set_font_size(30) diff --git a/test/signing/sign_pkcs12.pdf b/test/signing/sign_pkcs12.pdf index af46fad04..b682df594 100644 Binary files a/test/signing/sign_pkcs12.pdf and b/test/signing/sign_pkcs12.pdf differ diff --git a/test/signing/sign_pkcs12_with_link.pdf b/test/signing/sign_pkcs12_with_link.pdf index bec60dd74..261e50f66 100644 Binary files a/test/signing/sign_pkcs12_with_link.pdf and b/test/signing/sign_pkcs12_with_link.pdf differ diff --git a/test/table/table_with_cell_fill.pdf b/test/table/table_with_cell_fill.pdf index af6346c13..5ee144135 100644 Binary files a/test/table/table_with_cell_fill.pdf and b/test/table/table_with_cell_fill.pdf differ diff --git a/test/table/table_with_cell_overflow.pdf b/test/table/table_with_cell_overflow.pdf new file mode 100644 index 000000000..c9d1c5e6e Binary files /dev/null and b/test/table/table_with_cell_overflow.pdf differ diff --git a/test/table/table_with_headings_styled.pdf b/test/table/table_with_headings_styled.pdf index 1a3232048..a2202d713 100644 Binary files a/test/table/table_with_headings_styled.pdf and b/test/table/table_with_headings_styled.pdf differ diff --git a/test/table/table_with_images_and_img_fill_width.pdf b/test/table/table_with_images_and_img_fill_width.pdf index b97b74c3e..391c8e31d 100644 Binary files a/test/table/table_with_images_and_img_fill_width.pdf and b/test/table/table_with_images_and_img_fill_width.pdf differ diff --git a/test/table/table_with_multiline_cells_and_images.pdf b/test/table/table_with_multiline_cells_and_images.pdf index 85210326b..d4b1a85eb 100644 Binary files a/test/table/table_with_multiline_cells_and_images.pdf and b/test/table/table_with_multiline_cells_and_images.pdf differ diff --git a/test/table/table_with_ttf_font_and_headings.pdf b/test/table/table_with_ttf_font_and_headings.pdf index e2a76f407..d7de38769 100644 Binary files a/test/table/table_with_ttf_font_and_headings.pdf and b/test/table/table_with_ttf_font_and_headings.pdf differ diff --git a/test/table/test_table.py b/test/table/test_table.py index 136eb4624..b3bb35fcb 100644 --- a/test/table/test_table.py +++ b/test/table/test_table.py @@ -118,6 +118,7 @@ def test_table_with_multiline_cells(tmp_path): row = table.row() for datum in data_row: row.cell(datum) + assert pdf.pages_count == 2 assert_pdf_equal(pdf, HERE / "table_with_multiline_cells.pdf", tmp_path) @@ -130,6 +131,7 @@ def test_table_with_multiline_cells_and_fixed_row_height(tmp_path): row = table.row() for datum in data_row: row.cell(datum) + assert pdf.pages_count == 2 assert_pdf_equal( pdf, HERE / "table_with_multiline_cells_and_fixed_row_height.pdf", tmp_path ) @@ -180,8 +182,11 @@ def test_table_with_multiline_cells_and_without_headings(tmp_path): row = table.row() for datum in data_row: row.cell(datum) + assert pdf.pages_count == 4 assert_pdf_equal( - pdf, HERE / "table_with_multiline_cells_and_without_headings.pdf", tmp_path + pdf, + HERE / "table_with_multiline_cells_and_without_headings.pdf", + tmp_path, ) @@ -209,8 +214,11 @@ def test_table_with_multiline_cells_and_split_over_3_pages(tmp_path): row = table.row() for datum in data_row: row.cell(datum) + assert pdf.pages_count == 4 assert_pdf_equal( - pdf, HERE / "table_with_multiline_cells_and_split_over_3_pages.pdf", tmp_path + pdf, + HERE / "table_with_multiline_cells_and_split_over_3_pages.pdf", + tmp_path, ) @@ -351,3 +359,23 @@ def test_table_with_ttf_font_and_headings_but_missing_bold_font(): str(error.value) == "Using font emphasis 'B' in table headings require the corresponding font style to be added using add_font()" ) + + +def test_table_with_cell_overflow(tmp_path): + pdf = FPDF() + pdf.set_font("Times", size=30) + pdf.add_page() + with pdf.table(width=pdf.epw / 2, col_widths=(1, 2, 1)) as table: + row = table.row() + row.cell("left") + row.cell("center") + row.cell("right") # triggers header cell overflow on last column + row = table.row() + row.cell("A1") + row.cell("A2") + row.cell("A33333333") # triggers cell overflow on last column + row = table.row() + row.cell("B1") + row.cell("B2") + row.cell("B3") + assert_pdf_equal(pdf, HERE / "table_with_cell_overflow.pdf", tmp_path) diff --git a/test/test_recorder.py b/test/test_recorder.py index 6e138c394..5f8b543ca 100644 --- a/test/test_recorder.py +++ b/test/test_recorder.py @@ -1,7 +1,7 @@ from fpdf import FPDF from fpdf.recorder import FPDFRecorder -from test.conftest import assert_pdf_equal, EPOCH +from test.conftest import assert_pdf_equal, EPOCH, LOREM_IPSUM def init_pdf(): @@ -48,3 +48,14 @@ def test_recorder_replay_ok(tmp_path): def test_recorder_override_accept_page_break_ok(): recorder = FPDFRecorder(init_pdf(), accept_page_break=False) assert recorder.accept_page_break is False + + +def test_recorder_preserve_pages_count(): + pdf = init_pdf() + pdf.set_y(250) + assert pdf.pages_count == 1 + with pdf.offset_rendering() as recorder: + pdf.multi_cell(txt=LOREM_IPSUM, w=pdf.epw) + assert pdf.pages_count == 2 + assert recorder.page_break_triggered + assert pdf.pages_count == 1 diff --git a/test/text/test_multi_cell.py b/test/text/test_multi_cell.py index 02319fa74..30095fa52 100644 --- a/test/text/test_multi_cell.py +++ b/test/text/test_multi_cell.py @@ -249,7 +249,10 @@ def test_multi_cell_split_only(): # discussion 314 "reprehenderit anim nostrud", "dolore sed ut", ] - assert pdf.multi_cell(w=0, h=LINE_HEIGHT, txt=text, split_only=True) == expected + with pytest.warns( + DeprecationWarning, match='The parameter "split_only" is deprecated.' + ): + assert pdf.multi_cell(w=0, h=LINE_HEIGHT, txt=text, split_only=True) == expected def test_multi_cell_with_empty_contents(tmp_path): # issue 349 diff --git a/test/text/test_unbreakable.py b/test/text/test_unbreakable.py index f5f8d2f2c..8ecdb8969 100644 --- a/test/text/test_unbreakable.py +++ b/test/text/test_unbreakable.py @@ -1,6 +1,6 @@ from pathlib import Path -import fpdf +from fpdf import FPDF, FPDFException from test.conftest import assert_pdf_equal import pytest @@ -18,7 +18,7 @@ def test_multi_cell_table_unbreakable(tmp_path): # issue 111 - pdf = fpdf.FPDF() + pdf = FPDF() pdf.add_page() pdf.set_font("Times", size=16) line_height = pdf.font_size * 2 @@ -51,7 +51,7 @@ def test_multi_cell_table_unbreakable2(tmp_path): # issue 120 - 2nd snippet "G": "test_lin", "H": "test_lin", } - pdf = fpdf.FPDF() + pdf = FPDF() pdf.add_page() pdf.set_margins(20, 20) pdf.set_font("Times", "B", size=7) @@ -90,7 +90,9 @@ def test_multi_cell_table_unbreakable2(tmp_path): # issue 120 - 2nd snippet def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359 - pdf = fpdf.FPDF("P", "mm", "A4") + expected_warn = 'The parameter "split_only" is deprecated.' + + pdf = FPDF("P", "mm", "A4") pdf.set_auto_page_break(True, 20) pdf.set_font("Helvetica", "", 10) pdf.add_page() @@ -117,17 +119,18 @@ def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359 for row in data: max_no_of_lines_in_cell = 1 for cell in row: - result = pdf.multi_cell( - cell_width, - l_height, - cell, - border=1, - align="L", - new_x="RIGHT", - new_y="TOP", - max_line_height=l_height, - split_only=True, - ) + with pytest.warns(DeprecationWarning, match=expected_warn): + result = pdf.multi_cell( + cell_width, + l_height, + cell, + border=1, + align="L", + new_x="RIGHT", + new_y="TOP", + max_line_height=l_height, + split_only=True, + ) no_of_lines_in_cell = len(result) if no_of_lines_in_cell > max_no_of_lines_in_cell: max_no_of_lines_in_cell = no_of_lines_in_cell @@ -169,17 +172,18 @@ def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359 for row in data: max_no_of_lines_in_cell = 1 for cell in row: - result = doc.multi_cell( - cell_width, - l_height, - cell, - border=1, - align="L", - new_x="RIGHT", - new_y="TOP", - max_line_height=l_height, - split_only=True, - ) + with pytest.warns(DeprecationWarning, match=expected_warn): + result = doc.multi_cell( + cell_width, + l_height, + cell, + border=1, + align="L", + new_x="RIGHT", + new_y="TOP", + max_line_height=l_height, + split_only=True, + ) no_of_lines_in_cell = len(result) if no_of_lines_in_cell > max_no_of_lines_in_cell: max_no_of_lines_in_cell = no_of_lines_in_cell @@ -220,26 +224,26 @@ def test_multi_cell_table_unbreakable_with_split_only(tmp_path): # issue 359 def test_unbreakable_with_local_context(): # discussion 557 - pdf = fpdf.FPDF() + pdf = FPDF() pdf.set_font("Helvetica", "", 10) pdf.add_page() pdf.set_y(270) # Set position so that adding a cell triggers a page break - with pytest.raises(fpdf.FPDFException): + with pytest.raises(FPDFException): with pdf.unbreakable() as doc: with doc.local_context(fill_opacity=0.3): doc.cell(doc.epw, 10, "Cell text content", border=1, fill=True) pdf.set_y(270) # Set position so that adding a cell triggers a page break - with pytest.raises(fpdf.FPDFException): + with pytest.raises(FPDFException): with pdf.unbreakable() as doc: with doc.local_context(text_color=(255, 0, 0)): doc.cell(doc.epw, 10, "Cell text content", border=1) def test_unbreakable_with_get_y(): # discussion 557 - pdf = fpdf.FPDF() + pdf = FPDF() pdf.set_font("Helvetica", "", 10) pdf.add_page() pdf.set_y(270) # Set position so that adding a cell triggers a page break - with pytest.raises(fpdf.FPDFException): + with pytest.raises(FPDFException): with pdf.unbreakable() as doc: doc.cell(doc.epw, 10, f"doc.get_y(): {doc.get_y()}", border=1)