diff --git a/pyWebLayout/concrete/table.py b/pyWebLayout/concrete/table.py index a750e06..bb375f2 100644 --- a/pyWebLayout/concrete/table.py +++ b/pyWebLayout/concrete/table.py @@ -108,21 +108,34 @@ class TableCellRenderer(Box): return None # Cell rendering is done directly on the page def _render_cell_content(self, x: int, y: int, width: int, height: int): - """Render the content inside the cell (text and images).""" - from PIL import ImageFont + """Render the content inside the cell (text and images) with line wrapping.""" + from pyWebLayout.concrete.text import Line, Text + from pyWebLayout.style.fonts import Font + from pyWebLayout.style import FontWeight, Alignment current_y = y + 2 + available_height = height - 4 # Account for top/bottom padding - # Get font - try: - if self._is_header_section and self._style.header_text_bold: - font = ImageFont.truetype( - "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 12) - else: - font = ImageFont.truetype( - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12) - except BaseException: - font = ImageFont.load_default() + # Create font for the cell + font_size = 12 + font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" + if self._is_header_section and self._style.header_text_bold: + font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" + + font = Font( + font_path=font_path, + font_size=font_size, + weight=FontWeight.BOLD if self._is_header_section and self._style.header_text_bold else FontWeight.NORMAL + ) + + # Word spacing constraints (min, max) + min_spacing = int(font_size * 0.25) + max_spacing = int(font_size * 0.5) + word_spacing = (min_spacing, max_spacing) + + # Line height (baseline spacing) + line_height = font_size + 4 + ascent, descent = font.font.getmetrics() # Render each block in the cell for block in self._cell.blocks(): @@ -131,38 +144,83 @@ class TableCellRenderer(Box): current_y = self._render_image_in_cell( block, x, current_y, width, height - (current_y - y)) elif isinstance(block, (Paragraph, Heading)): - # Extract and render text - words = [] + # Get words from the block word_items = block.words() if callable(block.words) else block.words - for word in word_items: - if hasattr(word, 'text'): - words.append(word.text) - elif isinstance(word, tuple) and len(word) >= 2: - word_obj = word[1] - if hasattr(word_obj, 'text'): - words.append(word_obj.text) + words = list(word_items) - if words: - text = " ".join(words) - if current_y <= y + height - 15: - self._draw.text((x + 2, current_y), text, - fill=(0, 0, 0), font=font) - current_y += 16 + if not words: + continue + + # Layout words using Line objects with wrapping + word_index = 0 + pretext = None + + while word_index < len(words): + # Check if we have space for another line + if current_y + ascent + descent > y + available_height: + break # No more space in cell + + # Create a new line + line = Line( + spacing=word_spacing, + origin=(x + 2, current_y), + size=(width - 4, line_height), + draw=self._draw, + font=font, + halign=Alignment.LEFT + ) + + # Add words to this line until it's full + line_has_content = False + while word_index < len(words): + word = words[word_index] + + # Handle word tuples (index, word_obj) + if isinstance(word, tuple) and len(word) >= 2: + word_obj = word[1] + else: + word_obj = word + + # Try to add word to line + success, overflow = line.add_word(word_obj, pretext) + pretext = None # Clear pretext after use + + if success: + line_has_content = True + if overflow: + # Word was hyphenated, carry over to next line + pretext = overflow + word_index += 1 + else: + # Word doesn't fit on this line + if not line_has_content: + # Even first word doesn't fit, force it anyway to avoid infinite loop + word_index += 1 + break + + # Render the line if it has content + if line_has_content or len(line.text_objects) > 0: + line.render() + current_y += line_height if current_y > y + height - 10: # Don't overflow cell break # If no structured content, try to get any text representation if current_y == y + 2 and hasattr(self._cell, '_text_content'): + # Use simple text rendering for fallback case + from PIL import ImageFont + try: + pil_font = ImageFont.truetype(font_path, font_size) + except BaseException: + pil_font = ImageFont.load_default() + self._draw.text( - (x + 2, - current_y), + (x + 2, current_y), self._cell._text_content, - fill=( - 0, - 0, - 0), - font=font) + fill=(0, 0, 0), + font=pil_font + ) def _render_image_in_cell(self, image_block: AbstractImage, x: int, y: int, max_width: int, max_height: int) -> int: