Closed johann-su closed 1 year ago
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?
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)
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 😉
@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 :)
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?
@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.
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!
For reference, @RubendeBruin is working on an implementation to provide vertical alignement for table()
cells in https://github.com/PyFPDF/fpdf2/pull/797
Reviews & comments on this PR are welcome!
@johann-su Vertical alignment functionality has been merged into master and will be included in the next release.
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.