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

Vertically align Text in a Cell or Multicell #210

Closed
johann-su opened this issue Aug 25, 2021 · 9 comments
Closed

Vertically align Text in a Cell or Multicell #210

johann-su opened this issue Aug 25, 2021 · 9 comments

Comments

@johann-su
Copy link

Is there an option to change the vertical alignment of text inside a multicell from center to top or bottom?

I am using them to create a table and it would be very helpful to have the ability to change the vertical alignment of the text inside the table rows.

Bildschirmfoto 2021-08-25 um 20 33 55

@johann-su johann-su changed the title Vertically align Text in a Multicell Vertically align Text in a Cell or Multicell Aug 25, 2021
@Lucas-C
Copy link
Member

Lucas-C commented Aug 25, 2021

Hi @johann-su

Interesting question: no, there is currently no way to control vertical alignement in cells.

There are currently several methods to render tables with fpdf2: https://pyfpdf.github.io/fpdf2/Tables.html

Could you share a code snippet of what you are using, in order to see what can be done?

@johann-su
Copy link
Author

johann-su commented Aug 25, 2021

I used this as a starting point and added some functionality to it, the most important of them being to compute the height of each row. I've done this similar to how the height of a multicell is calculated within the library.

Here is the complete code though it is quite long.

from fpdf import FPDF
# https://github.com/bvalgard/create-pdf-with-python-fpdf2/blob/main/create_table_fpdf2.py

class PdfTable(FPDF):
    def create_table(self, table_data, title='', data_size = 10, header_size=10, title_size=12, align_data='L', align_header='L', cell_width='even', x_start='x_default',emphasize_data=[], emphasize_style=None,emphasize_color=(0,0,0), border=False, footer=[], align_footer="R", footer_style="B"):
        """
        table_data: 
                    list of lists with first element being list of headers
        title: 
                    (Optional) title of table (optional)
        data_size: 
                    the font size of table data
        title_size: 
                    the font size fo the title of the table
        align_data: 
                    align table data
                    L = left align
                    C = center align
                    R = right align
        align_header: 
                    align table data
                    L = left align
                    C = center align
                    R = right align
        cell_width: 
                    even: evenly distribute cell/column width
                    uneven: base cell size on lenght of cell/column items
                    int: int value for width of each cell/column
                    list of ints: list equal to number of columns with the widht of each cell / column
        x_start: 
                    where the left edge of table should start
        emphasize_data:  
                    which data elements are to be emphasized - pass as list 
                    emphasize_style: the font style you want emphaized data to take
                    emphasize_color: emphasize color (if other than black) 
        
        """
        default_style = self.font_style
        if emphasize_style == None:
            emphasize_style = default_style
        line_height = self.font_size*1.2
        header = table_data[0]
        data = table_data[1:]

        self.set_font(size=title_size)

        def _char_width(font, char):
            cw = font["cw"]
            try:
                width = cw[char]
            except IndexError:
                width = font["desc"].get("MissingWidth") or 500
            if width == 65535:
                width = 0
            return width

        # Get Width of Columns
        def get_col_widths():
            col_width = cell_width
            if col_width == 'even':
                col_width = self.epw / len(table_data[0]) - 1  # distribute content evenly   # epw = effective page width (width of page not including margins)
            elif col_width == 'uneven':
                col_widths = []

                # searching through columns for largest sized cell (not rows but cols)
                for col in range(len(table_data[0])): # for every row
                    longest = 0 
                    for row in range(len(table_data)):
                        cell_value = str(table_data[row][col])
                        value_length = self.get_string_width(cell_value)
                        if value_length > longest:
                            longest = value_length
                    col_widths.append(longest + 4) # add 4 for padding
                col_width = col_widths
                ### compare columns 

            elif isinstance(cell_width, list):
                col_width = cell_width  # TODO: convert all items in list to int        
            else:
                # TODO: Add try catch
                col_width = int(col_width)
            return col_width

        # get the height of a multi_cell
        def get_row_height(txt, index=0):
            # Calculate text length
            txt = self.normalize_text(txt)
            s = txt.replace("\r", "")
            normalized_string_length = len(s)
            if normalized_string_length > 0 and s[-1] == "\n":
                normalized_string_length -= 1

            i = 0
            line_len = 0
            lines_count = 1
            while i < normalized_string_length:
                # Get next character
                c = s[i]

                if isinstance(get_col_widths(), list):
                    max_width = (get_col_widths()[index] - 2 * self.c_margin) * 1000 / self.font_size
                else:
                    max_width = (get_col_widths() - 2 * self.c_margin) * 1000 / self.font_size

                if self.unifontsubset:
                    line_len += self.get_string_width(c, True) / self.font_size * 1000
                else:
                    line_len += _char_width(self.current_font, c)

                # Explicit line break
                if c == "\n":
                    line_len = 0
                    lines_count += 1

                # Automatic line break
                if line_len > max_width:
                    line_len = 0
                    lines_count +=1
                
                i = i + 1

            return (lines_count+1) * line_height

        def get_title_multicell():
            self.multi_cell(0, line_height, title, border=0, align='j', ln=3)
            self.ln(line_height) # move cursor back to the left margin

        def get_emphazised_datacell(datum, col_width, cell_height, align=align_data, border=border):
            self.set_text_color(*emphasize_color)
            self.set_font(style=emphasize_style)
            if border == True:
                self.multi_cell(col_width, cell_height, datum, border=1, align=align, ln=3, max_line_height=line_height)
            elif border == False:
                self.multi_cell(col_width, cell_height, datum, border=0, align=align, ln=3, max_line_height=line_height)
            self.set_text_color(0,0,0)
            self.set_font(style=default_style)

        def get_regular_datacell(datum, col_width, cell_height, align=align_data, border=border):
            if border == True:
                self.multi_cell(col_width, cell_height, datum, border=1, align=align, ln=3, max_line_height=line_height)
            elif border == False: 
                self.multi_cell(col_width, cell_height, datum, border=0, align=align, ln=3,max_line_height=line_height)

        def get_x_start():
            # Get starting position of x
            # Determin width of table to get x starting point for centred table
            if x_start == 'C':
                table_width = 0
                if isinstance(col_width, list):
                    for width in col_width:
                        table_width += width
                else: # need to multiply cell width by number of cells to get table width 
                    table_width = col_width * len(table_data[0])
                # Get x start by subtracting table width from pdf width and divide by 2 (margins)
                margin_width = self.w - table_width
                # TODO: Check if table_width is larger than pdf width

                center_table = margin_width / 2 # only want width of left margin not both
                x_start_new = center_table
                self.set_x(x_start)
            elif isinstance(x_start, int):
                self.set_x(x_start)
            elif x_start == 'x_default':
                x_start_new = self.set_x(self.l_margin)

            return x_start_new

        def get_cell_height(row):
            cell_height = line_height
            for i, text in enumerate(row):
                new_height = get_row_height(text, i)
                if new_height > cell_height:
                    cell_height = new_height

            return cell_height

        col_width = get_col_widths()

        # TABLE CREATION #
        x_start = get_x_start()
        # add title
        if title != '':
            get_title_multicell()

        # add header
        y1 = self.get_y()
        if x_start:
            x_left = x_start
        else:
            x_left = self.get_x()
        x_right = self.epw + x_left
        
        self.set_font(size=header_size)
        if not isinstance(col_width, list):
            if x_start:
                self.set_x(x_start)

            cell_height = get_cell_height(header)

            for datum in header:
                get_regular_datacell(datum, col_width, cell_height, align=align_header)
                x_right = self.get_x()
            self.ln(cell_height) # move cursor back to the left margin
             # add line beneeth header
            y2 = self.get_y()
            self.line(x_left,y1,x_right,y1)
            self.line(x_left,y2,x_right,y2)
            
            # add data
            self.set_font(size=data_size)
            for row in data:
                if x_start: # not sure if I need this
                    self.set_x(x_start)

                cell_height = get_cell_height(row)

                for datum in row:
                    if datum in emphasize_data:
                        get_emphazised_datacell(datum, col_width, cell_height)
                    else:
                        get_regular_datacell(datum, col_width, cell_height)
                self.ln(cell_height) # move cursor back to the left margin

        else:
            if x_start:
                self.set_x(x_start)

            cell_height = get_cell_height(header)

            for i, datum in enumerate(header):
                get_regular_datacell(datum, col_width[i], cell_height, align=align_header)
                x_right = self.get_x()
            self.ln(cell_height) # move cursor back to the left margin
            # add line beneeth header
            y2 = self.get_y()
            self.line(x_left,y1,x_right,y1)
            self.line(x_left,y2,x_right,y2)
            
            # add data
            self.set_font(size=data_size)
            for i, row in enumerate(data):
                if x_start:
                    self.set_x(x_start)

                cell_height = get_cell_height(row)

                for i, datum in enumerate(row):
                    if not isinstance(datum, str):
                        datum = str(datum)
                    adjusted_col_width = col_width[i]
                    if datum in emphasize_data:
                        get_emphazised_datacell(datum, adjusted_col_width, cell_height)
                    else:
                        get_regular_datacell(datum, adjusted_col_width, cell_height)
                self.ln(cell_height) # move cursor back to the left margin
        y3 = self.get_y()
        self.line(x_left,y3,x_right,y3)
        
        # footer
        # add data
        self.set_font(style=footer_style, size=data_size)
        for row in footer:
            if isinstance(cell_width, list):
                if x_start:
                    self.set_x(x_start)

                cell_height = get_cell_height(row)

                for i, datum in enumerate(row):
                    if not isinstance(datum, str):
                        datum = str(datum)
                    adjusted_col_width = col_width[i]
                    if datum in emphasize_data:
                        get_emphazised_datacell(datum, adjusted_col_width, cell_height, align=align_footer, border=False)
                    else:
                        get_regular_datacell(datum, adjusted_col_width, cell_height, align=align_footer, border=False)
                self.ln(cell_height) # move cursor back to the left margin
            else:
                if x_start: # not sure if I need this
                    self.set_x(x_start)

                cell_height = get_cell_height(row)

                for datum in row:
                    if datum in emphasize_data:
                        get_emphazised_datacell(datum, col_width, cell_height, align=align_footer)
                    else:
                        get_regular_datacell(datum, col_width, cell_height, align=align_footer)
                self.ln(cell_height) # move cursor back to the left margin
        y4 = self.get_y()
        self.line(x_left,y4,x_right,y4)

I then use the class like this:

pdf = PdfTable()
pdf.create_table(data, border=True, align_data="L", data_size=9, emphasize_style="B", cell_width=[10, 15, 25, 25, 60, 20, 20], footer=self.footer)

@Lucas-C
Copy link
Member

Lucas-C commented Aug 26, 2021

OK I see. Thanks for sharing your code.

@bvalgard code is well crafted, but it is not part of fpdf2.
Hence we won't be able to help you much with it here...

You can try reaching him about this "vertical alignement" feature on bvalgard/create-pdf-with-python-fpdf2.

Also, I'd be open to introducing a FPDF.table method to fpdf2!
The method could include this "vertical alignement" feature.
PRs are welcome 😉

@johann-su
Copy link
Author

johann-su commented Aug 26, 2021

@Lucas-C
But isn't it a valid use case to have a cell with a specific height (in my case cell_height) and to then limit the line height with max_line_height?
Since this is all default behavior of the library I thought the issue would make more sense here than in @bvalgard's repo?

pdf.multi_cell(col_width, cell_height, datum, border=1, align=align, ln=3, max_line_height=line_height)

I See what I can do regarding the pr :)

@Lucas-C
Copy link
Member

Lucas-C commented Aug 26, 2021

Yes, this seems a valid use case, and max_line_height is already supported:
https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.multi_cell

Do you mean that this parameter does not behave as it should?

@johann-su
Copy link
Author

@Lucas-C
Since it is possible to create cells that are bigger (vertically) than the text within them I think there should be an option to align the text to the top, bottom or center of the cell. I use it this functionality to create a table, but I guess there are other use cases where it would be helpful to be able to do this.

You could add a pdf.rect and place the text within the rectangle but that adds quite a lot of complexity to the code for a quite simple goal in my opinion.

@Lucas-C
Copy link
Member

Lucas-C commented Oct 1, 2021

Thanks for your explanation. I think I understand precisely the feature you want and why.
I agree that an option to align the text to the top, bottom or center of the cell
would be great. Again, PRs are welcome to implement that!

@Lucas-C
Copy link
Member

Lucas-C commented Jun 5, 2023

For reference, @RubendeBruin is working on an implementation to provide vertical alignement for table() cells in #797

Reviews & comments on this PR are welcome!

@RubendeBruin
Copy link

@johann-su
Vertical alignment functionality has been merged into master and will be included in the next release.

@Lucas-C Lucas-C closed this as completed Sep 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants