Last active
September 17, 2024 19:18
-
-
Save xiaopc/324acb627e6f1f019ab60b0ec0e355aa to your computer and use it in GitHub Desktop.
Draw a table using only Pillow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PIL import Image, ImageFont, ImageDraw | |
from collections import namedtuple | |
def position_tuple(*args): | |
Position = namedtuple('Position', ['top', 'right', 'bottom', 'left']) | |
if len(args) == 0: | |
return Position(0, 0, 0, 0) | |
elif len(args) == 1: | |
return Position(args[0], args[0], args[0], args[0]) | |
elif len(args) == 2: | |
return Position(args[0], args[1], args[0], args[1]) | |
elif len(args) == 3: | |
return Position(args[0], args[1], args[2], args[1]) | |
else: | |
return Position(args[0], args[1], args[2], args[3]) | |
def draw_table(table, header=[], font=ImageFont.load_default(), cell_pad=(20, 10), margin=(10, 10), align=None, colors={}, stock=False): | |
""" | |
Draw a table using only Pillow | |
table: an 2d list, must be str | |
header: turple or list, must be str | |
font: an ImageFont object | |
cell_pad: padding for cell, (top_bottom, left_right) | |
margin: margin for table, css-like shorthand | |
align: None or list, 'l'/'c'/'r' for left/center/right, length must be the max count of columns | |
colors: dict, as follows | |
stock: bool, set red/green font color for cells start with +/- | |
""" | |
_color = { | |
'bg': 'white', | |
'cell_bg': 'white', | |
'header_bg': 'gray', | |
'font': 'black', | |
'rowline': 'black', | |
'colline': 'black', | |
'red': 'red', | |
'green': 'green', | |
} | |
_color.update(colors) | |
_margin = position_tuple(*margin) | |
table = table.copy() | |
if header: | |
table.insert(0, header) | |
row_max_hei = [0] * len(table) | |
col_max_wid = [0] * len(max(table, key=len)) | |
for i in range(len(table)): | |
for j in range(len(table[i])): | |
col_max_wid[j] = max(font.getsize(table[i][j])[0], col_max_wid[j]) | |
row_max_hei[i] = max(font.getsize(table[i][j])[1], row_max_hei[i]) | |
tab_width = sum(col_max_wid) + len(col_max_wid) * 2 * cell_pad[0] | |
tab_heigh = sum(row_max_hei) + len(row_max_hei) * 2 * cell_pad[1] | |
tab = Image.new('RGBA', (tab_width + _margin.left + _margin.right, tab_heigh + _margin.top + _margin.bottom), _color['bg']) | |
draw = ImageDraw.Draw(tab) | |
draw.rectangle([(_margin.left, _margin.top), (_margin.left + tab_width, _margin.top + tab_heigh)], | |
fill=_color['cell_bg'], width=0) | |
if header: | |
draw.rectangle([(_margin.left, _margin.top), (_margin.left + tab_width, _margin.top + row_max_hei[0] + cell_pad[1] * 2)], | |
fill=_color['header_bg'], width=0) | |
top = _margin.top | |
for row_h in row_max_hei: | |
draw.line([(_margin.left, top), (tab_width + _margin.left, top)], fill=_color['rowline']) | |
top += row_h + cell_pad[1] * 2 | |
draw.line([(_margin.left, top), (tab_width + _margin.left, top)], fill=_color['rowline']) | |
left = _margin.left | |
for col_w in col_max_wid: | |
draw.line([(left, _margin.top), (left, tab_heigh +_margin.top)], fill=_color['colline']) | |
left += col_w + cell_pad[0] * 2 | |
draw.line([(left, _margin.top), (left, tab_heigh + _margin.top)], fill=_color['colline']) | |
top, left = _margin.top + cell_pad[1], 0 | |
for i in range(len(table)): | |
left = _margin.left + cell_pad[0] | |
for j in range(len(table[i])): | |
color = _color['font'] | |
if stock: | |
if table[i][j].startswith('+'): | |
color = _color['red'] | |
elif table[i][j].startswith('-'): | |
color = _color['green'] | |
_left = left | |
if (align and align[j] == 'c') or (header and i == 0): | |
_left += (col_max_wid[j] - font.getsize(table[i][j])[0]) // 2 | |
elif align and align[j] == 'r': | |
_left += col_max_wid[j] - font.getsize(table[i][j])[0] | |
draw.text((_left, top), table[i][j], font=font, fill=color) | |
left += col_max_wid[j] + cell_pad[0] * 2 | |
top += row_max_hei[i] + cell_pad[1] * 2 | |
return tab |
this works very well
adjusted to using getbbox
instead of getsize
because of the deprecation. thank you so much!
def font_getsize(self, text):
left, top, right, bottom = self.getbbox(text)
return right - left, bottom - top
ImageFont.FreeTypeFont.getsize = font_getsize
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I express my gratitude, I used it in my project!