cell height now dynamic in tables
This commit is contained in:
+151
-20
@@ -472,28 +472,17 @@ class TableRenderer(Box):
|
||||
column_width = max(50, available_for_columns // num_columns)
|
||||
column_widths = [column_width] * num_columns
|
||||
|
||||
# Calculate row heights
|
||||
# Minimum height needs to account for:
|
||||
# - Font size (12px) + line height (4px) = 16px per line
|
||||
# - Cell padding (varies, but typically 10-20px top+bottom)
|
||||
# - At least 2 lines of text for wrapping
|
||||
# So minimum should be ~60px to accommodate padding + 2 lines
|
||||
header_height = 60 if any(1 for section,
|
||||
_ in all_rows if section == "header") else 0
|
||||
# Calculate row heights dynamically based on content
|
||||
header_height = self._calculate_row_height_for_section(
|
||||
all_rows, "header", column_widths) if any(
|
||||
1 for section, _ in all_rows if section == "header") else 0
|
||||
|
||||
# Check if any body rows contain images - if so, use larger height
|
||||
body_height = 60 # Increased from 30 to allow for text wrapping
|
||||
for section, row in all_rows:
|
||||
if section == "body":
|
||||
for cell in row.cells():
|
||||
for block in cell.blocks():
|
||||
if isinstance(block, AbstractImage):
|
||||
# Use larger height for rows with images
|
||||
body_height = max(body_height, 120)
|
||||
break
|
||||
body_height = self._calculate_row_height_for_section(
|
||||
all_rows, "body", column_widths)
|
||||
|
||||
footer_height = 60 if any(1 for section,
|
||||
_ in all_rows if section == "footer") else 0
|
||||
footer_height = self._calculate_row_height_for_section(
|
||||
all_rows, "footer", column_widths) if any(
|
||||
1 for section, _ in all_rows if section == "footer") else 0
|
||||
|
||||
row_heights = {
|
||||
"header": header_height,
|
||||
@@ -503,6 +492,148 @@ class TableRenderer(Box):
|
||||
|
||||
return (column_widths, row_heights)
|
||||
|
||||
def _calculate_row_height_for_section(
|
||||
self,
|
||||
all_rows: List,
|
||||
section: str,
|
||||
column_widths: List[int]) -> int:
|
||||
"""
|
||||
Calculate the maximum required height for rows in a specific section.
|
||||
|
||||
Args:
|
||||
all_rows: List of all rows in the table
|
||||
section: Section name ('header', 'body', or 'footer')
|
||||
column_widths: List of column widths
|
||||
|
||||
Returns:
|
||||
Maximum height needed for rows in this section
|
||||
"""
|
||||
from pyWebLayout.concrete.text import Text
|
||||
from pyWebLayout.style.fonts import Font
|
||||
from pyWebLayout.abstract.inline import Word as AbstractWord
|
||||
|
||||
# Font configuration
|
||||
font_size = 12
|
||||
line_height = font_size + 4
|
||||
padding = self._style.cell_padding
|
||||
vertical_padding = padding[0] + padding[2] # top + bottom
|
||||
horizontal_padding = padding[1] + padding[3] # left + right
|
||||
|
||||
max_height = 40 # Minimum height
|
||||
|
||||
for row_section, row in all_rows:
|
||||
if row_section != section:
|
||||
continue
|
||||
|
||||
row_max_height = 40 # Minimum for this row
|
||||
|
||||
for cell_idx, cell in enumerate(row.cells()):
|
||||
if cell_idx >= len(column_widths):
|
||||
continue
|
||||
|
||||
# Get cell width (accounting for colspan)
|
||||
cell_width = column_widths[cell_idx]
|
||||
if cell.colspan > 1 and cell_idx + \
|
||||
cell.colspan <= len(column_widths):
|
||||
cell_width = sum(
|
||||
column_widths[cell_idx:cell_idx + cell.colspan])
|
||||
cell_width += self._style.border_width * (cell.colspan - 1)
|
||||
|
||||
# Calculate content width (minus padding)
|
||||
content_width = cell_width - horizontal_padding - 4 # Extra margin
|
||||
|
||||
cell_height = vertical_padding + 4 # Base height with padding
|
||||
|
||||
# Analyze each block in the cell
|
||||
for block in cell.blocks():
|
||||
if isinstance(block, AbstractImage):
|
||||
# Images need more space
|
||||
cell_height = max(cell_height, 120)
|
||||
elif isinstance(block, (Paragraph, Heading)):
|
||||
# Calculate text wrapping height
|
||||
word_items = block.words() if callable(
|
||||
block.words) else block.words
|
||||
words = list(word_items)
|
||||
|
||||
if not words:
|
||||
continue
|
||||
|
||||
# Simulate text wrapping to count lines
|
||||
lines_needed = self._estimate_wrapped_lines(
|
||||
words, content_width, font_size)
|
||||
text_height = lines_needed * line_height
|
||||
cell_height = max(
|
||||
cell_height, text_height + vertical_padding + 4)
|
||||
|
||||
row_max_height = max(row_max_height, cell_height)
|
||||
|
||||
max_height = max(max_height, row_max_height)
|
||||
|
||||
return max_height
|
||||
|
||||
def _estimate_wrapped_lines(
|
||||
self,
|
||||
words: List,
|
||||
available_width: int,
|
||||
font_size: int) -> int:
|
||||
"""
|
||||
Estimate how many lines are needed to render the given words.
|
||||
|
||||
Args:
|
||||
words: List of word objects
|
||||
available_width: Available width for text
|
||||
font_size: Font size in pixels
|
||||
|
||||
Returns:
|
||||
Number of lines needed
|
||||
"""
|
||||
from pyWebLayout.concrete.text import Text
|
||||
from pyWebLayout.style.fonts import Font
|
||||
|
||||
# Create a temporary font for measurement
|
||||
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
font = Font(font_path=font_path, font_size=font_size)
|
||||
|
||||
# Word spacing (approximate)
|
||||
word_spacing = int(font_size * 0.25)
|
||||
|
||||
lines = 1
|
||||
current_line_width = 0
|
||||
|
||||
for word_item in words:
|
||||
# Handle word tuples (index, word_obj)
|
||||
if isinstance(word_item, tuple) and len(word_item) >= 2:
|
||||
word_obj = word_item[1]
|
||||
else:
|
||||
word_obj = word_item
|
||||
|
||||
# Extract text from the word
|
||||
word_text = word_obj.text if hasattr(
|
||||
word_obj, 'text') else str(word_obj)
|
||||
|
||||
# Measure word width
|
||||
word_width = font.font.getlength(word_text)
|
||||
|
||||
# Check if word fits on current line
|
||||
if current_line_width > 0: # Not first word on line
|
||||
needed_width = current_line_width + word_spacing + word_width
|
||||
if needed_width > available_width:
|
||||
# Need new line
|
||||
lines += 1
|
||||
current_line_width = word_width
|
||||
else:
|
||||
current_line_width = needed_width
|
||||
else:
|
||||
# First word on line
|
||||
if word_width > available_width:
|
||||
# Word needs to be hyphenated, assume it takes 1 line
|
||||
lines += 1
|
||||
current_line_width = 0
|
||||
else:
|
||||
current_line_width = word_width
|
||||
|
||||
return lines
|
||||
|
||||
def render(self) -> Image.Image:
|
||||
"""Render the complete table."""
|
||||
x, y = self._origin
|
||||
|
||||
Reference in New Issue
Block a user