diff --git a/docs/images/example_04_table_rendering.png b/docs/images/example_04_table_rendering.png index b0f5879..d363771 100644 Binary files a/docs/images/example_04_table_rendering.png and b/docs/images/example_04_table_rendering.png differ diff --git a/docs/images/example_11_table_text_wrapping.png b/docs/images/example_11_table_text_wrapping.png new file mode 100644 index 0000000..2706324 Binary files /dev/null and b/docs/images/example_11_table_text_wrapping.png differ diff --git a/docs/images/example_11b_simple_wrapping.png b/docs/images/example_11b_simple_wrapping.png new file mode 100644 index 0000000..39467d5 Binary files /dev/null and b/docs/images/example_11b_simple_wrapping.png differ diff --git a/examples/11_table_text_wrapping_demo.py b/examples/11_table_text_wrapping_demo.py new file mode 100644 index 0000000..0998f20 --- /dev/null +++ b/examples/11_table_text_wrapping_demo.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +""" +Table Text Wrapping Example + +This example demonstrates the line wrapping functionality in table cells: +- Tables with long text that wraps across multiple lines +- Automatic word wrapping within cell boundaries +- Hyphenation support for long words +- Multiple paragraphs per cell +- Comparison of narrow vs. wide columns + +Shows how the Line-based text layout system handles text overflow in tables. +""" + +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.concrete.page import Page +import sys +from pathlib import Path +from PIL import Image + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +def create_narrow_columns_example(): + """Create a table with narrow columns to show aggressive wrapping.""" + print(" - Narrow columns with text wrapping") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureDescriptionBenefits
Automatic Line WrappingText automatically wraps to fit within the available cell width, creating multiple lines as needed.Improves readability and prevents horizontal overflow in tables.
Hyphenation SupportLong words are intelligently hyphenated using pyphen library or brute-force splitting when necessary.Handles extraordinarily long words that wouldn't fit on a single line.
Multi-paragraph CellsEach cell can contain multiple paragraphs or headings, all properly wrapped.Allows rich content within table cells.
+ """ + + return html, "Text Wrapping in Narrow Columns" + + +def create_mixed_content_example(): + """Create a table with both short and long content.""" + print(" - Mixed content lengths") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Product Comparison
ProductShort DescriptionDetailed Features
Widget ProPremiumAdvanced functionality with enterprise-grade reliability, comprehensive warranty coverage, and dedicated customer support available around the clock.
Widget LiteBasicEssential features for everyday use with straightforward operation and minimal learning curve.
Widget MaxUltimateEverything from Widget Pro plus additional customization options, API integration capabilities, and advanced analytics dashboard.
+ """ + + return html, "Mixed Short and Long Content" + + +def create_technical_documentation_example(): + """Create a table like technical documentation.""" + print(" - Technical documentation style") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
API MethodParametersDescriptionReturn Value
render_table()table, origin, width, draw, styleRenders a table with automatic text wrapping in cells. Uses the Line class for intelligent word placement and hyphenation.Rendered table with calculated height and width properties.
add_word()word, pretextAttempts to add a word to the current line. If it doesn't fit, tries hyphenation strategies including pyphen and brute-force splitting.Tuple of (success, overflow_text) indicating whether word was added and any remaining text.
calculate_spacing()text_objects, width, min_spacing, max_spacingDetermines optimal spacing between words to achieve proper justification within the specified constraints.Calculated spacing value and position offset for alignment.
+ """ + + return html, "Technical Documentation Table" + + +def create_news_article_example(): + """Create a table with article-style content.""" + print(" - News article layout") + + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + +
DateHeadlineSummary
2024-01-15New Text Wrapping FeaturePyWebLayout now supports automatic line wrapping in table cells, bringing sophisticated text layout capabilities to table rendering. The implementation leverages the existing Line class infrastructure.
2024-01-10Hyphenation ImprovementsEnhanced hyphenation algorithms now include both dictionary-based pyphen hyphenation and intelligent brute-force splitting for edge cases.
2024-01-05Performance OptimizationTable rendering performance improved through better caching and reduced font object creation overhead.
+ """ + + return html, "News Article Layout" + + +def render_table_example(html, title, style_variant=0): + """Render a single table example.""" + from pyWebLayout.style import Font + from pyWebLayout.abstract.block import Table + + # Parse HTML + base_font = Font(font_size=12) + blocks = parse_html_string(html, base_font=base_font) + + # Find the table block + table = None + for block in blocks: + if isinstance(block, Table): + table = block + break + + if not table: + print(f" Warning: No table found in {title}") + return None + + # Create page style + page_style = PageStyle( + padding=(20, 20, 20, 20), + background_color=(255, 255, 255) + ) + + # Create page + page_size = (900, 600) + page = Page(size=page_size, style=page_style) + + # Create table style variants + table_styles = [ + # Default style + TableStyle( + border_width=1, + border_color=(0, 0, 0), + cell_padding=(8, 8, 8, 8), + header_bg_color=(240, 240, 240), + cell_bg_color=(255, 255, 255) + ), + # Blue header style + TableStyle( + border_width=2, + border_color=(70, 130, 180), + cell_padding=(10, 10, 10, 10), + header_bg_color=(176, 196, 222), + cell_bg_color=(245, 250, 255) + ), + # Minimal style + TableStyle( + border_width=1, + border_color=(200, 200, 200), + cell_padding=(6, 6, 6, 6), + header_bg_color=(250, 250, 250), + cell_bg_color=(255, 255, 255) + ), + ] + + table_style = table_styles[style_variant % len(table_styles)] + + # Create layouter and render table + layouter = DocumentLayouter(page) + layouter.layout_table(table, style=table_style) + + # Get the rendered canvas + _ = page.draw # Ensure canvas exists + img = page._canvas + + return img + + +def combine_examples(examples): + """Combine multiple example images into one.""" + images = [] + titles = [] + + for html, title in examples: + img = render_table_example(html, title) + if img: + images.append(img) + titles.append(title) + + if not images: + return None + + # Calculate combined image size + max_width = max(img.width for img in images) + total_height = sum(img.height for img in images) + 40 * len(images) # Extra space between images + + # Create combined image + combined = Image.new('RGB', (max_width, total_height), color=(255, 255, 255)) + + # Paste images + y_offset = 20 + for img in images: + combined.paste(img, (0, y_offset)) + y_offset += img.height + 40 + + return combined + + +def main(): + """Run the table text wrapping example.""" + print("\nTable Text Wrapping Example") + print("=" * 50) + + # Create examples + print("\n Creating table examples...") + examples = [ + create_narrow_columns_example(), + create_mixed_content_example(), + create_technical_documentation_example(), + create_news_article_example(), + ] + + print("\n Rendering table examples...") + combined_image = combine_examples(examples) + + if combined_image: + # Save the output + output_dir = Path(__file__).parent.parent / 'docs' / 'images' + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / 'example_11_table_text_wrapping.png' + + combined_image.save(str(output_path)) + + print("\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {combined_image.width}x{combined_image.height} pixels") + print(f" Created {len(examples)} table examples with text wrapping") + else: + print("\n✗ Failed to generate examples") + + +if __name__ == '__main__': + main() diff --git a/examples/11b_simple_table_wrapping.py b/examples/11b_simple_table_wrapping.py new file mode 100644 index 0000000..a3af467 --- /dev/null +++ b/examples/11b_simple_table_wrapping.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Simple Table Text Wrapping Example + +A minimal example showing text wrapping in table cells. +Perfect for quick testing and demonstration. +""" + +import sys +from pathlib import Path + +# Add pyWebLayout to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from pyWebLayout.io.readers.html_extraction import parse_html_string +from pyWebLayout.layout.document_layouter import DocumentLayouter +from pyWebLayout.style.page_style import PageStyle +from pyWebLayout.style import Font +from pyWebLayout.concrete.table import TableStyle +from pyWebLayout.concrete.page import Page +from pyWebLayout.abstract.block import Table + + +def main(): + """Create a simple table with text wrapping.""" + print("\nSimple Table Text Wrapping Example") + print("=" * 50) + + # HTML with a table containing long text + html = """ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Text Wrapping Demonstration
Column 1Column 2Column 3
This is a cell with quite a lot of text that will need to wrap across multiple lines.Short textAnother cell with enough content to demonstrate the automatic line wrapping functionality.
Cell AThis middle cell contains a paragraph with several words that should wrap nicely within the available space.Cell C
Words like supercalifragilisticexpialidocious might need hyphenation.Normal textThe wrapping algorithm handles both regular word wrapping and hyphenation seamlessly.
+ """ + + print("\n Parsing HTML and creating table...") + + # Parse HTML + base_font = Font(font_size=12) + blocks = parse_html_string(html, base_font=base_font) + + # Find table + table = None + for block in blocks: + if isinstance(block, Table): + table = block + break + + if not table: + print(" ✗ No table found!") + return + + print(" ✓ Table parsed successfully") + + # Create page + page_style = PageStyle( + padding=(30, 30, 30, 30), + background_color=(255, 255, 255) + ) + page = Page(size=(800, 600), style=page_style) + + # Create table style + table_style = TableStyle( + border_width=2, + border_color=(70, 130, 180), + cell_padding=(10, 10, 10, 10), + header_bg_color=(176, 196, 222), + cell_bg_color=(245, 250, 255) + ) + + print(" Rendering table with text wrapping...") + + # Layout and render + layouter = DocumentLayouter(page) + layouter.layout_table(table, style=table_style) + + # Get rendered image + _ = page.draw + img = page._canvas + + # Save output + output_path = Path(__file__).parent.parent / 'docs' / 'images' / 'example_11b_simple_wrapping.png' + output_path.parent.mkdir(parents=True, exist_ok=True) + img.save(str(output_path)) + + print(f"\n✓ Example completed!") + print(f" Output saved to: {output_path}") + print(f" Image size: {img.width}x{img.height} pixels") + print(f"\n The table demonstrates:") + print(f" • Automatic line wrapping in cells") + print(f" • Proper word spacing and alignment") + print(f" • Hyphenation for very long words") + print(f" • Multi-line text within cell boundaries") + + +if __name__ == '__main__': + main() diff --git a/examples/README.md b/examples/README.md index 4f34504..08bfc5c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -174,6 +174,31 @@ Demonstrates all 14 FormFieldType variations: ![Comprehensive Forms Example](../docs/images/example_10_forms.png) +### 11. Table Text Wrapping (NEW) ✅ +**`11_table_text_wrapping_demo.py`** - Automatic line wrapping in table cells + +```bash +python 11_table_text_wrapping_demo.py +``` + +**Simple Version:** `11b_simple_table_wrapping.py` - Quick demonstration + +Demonstrates: +- **Automatic line wrapping** - Text wraps across multiple lines within cells +- **Word hyphenation** - Long words are intelligently hyphenated +- **Narrow columns** - Aggressive wrapping for tight spaces +- **Mixed content** - Both short and long text in the same table +- **Technical documentation** - API reference style tables +- **News layouts** - Article-style table content + +**Implementation:** Uses the Line class from `pyWebLayout.concrete.text` with: +- Word-by-word fitting with intelligent spacing +- Pyphen-based dictionary hyphenation +- Brute-force splitting for edge cases +- Proper baseline alignment and metrics + +![Table Text Wrapping Example](../docs/images/example_11_table_text_wrapping.png) + --- ## Advanced Examples @@ -207,6 +232,8 @@ python 06_functional_elements_demo.py python 08_pagination_demo.py # Multi-page documents python 09_link_navigation_demo.py # All link types python 10_forms_demo.py # All form field types +python 11_table_text_wrapping_demo.py # Table cell text wrapping +python 11b_simple_table_wrapping.py # Simple wrapping demo ``` Output images are saved to the `docs/images/` directory. diff --git a/pyWebLayout/concrete/table.py b/pyWebLayout/concrete/table.py index bb375f2..ff7d303 100644 --- a/pyWebLayout/concrete/table.py +++ b/pyWebLayout/concrete/table.py @@ -145,17 +145,36 @@ class TableCellRenderer(Box): block, x, current_y, width, height - (current_y - y)) elif isinstance(block, (Paragraph, Heading)): # Get words from the block + from pyWebLayout.abstract.inline import Word as AbstractWord + word_items = block.words() if callable(block.words) else block.words words = list(word_items) if not words: continue + # Create new Word objects with the table cell's font + # The words from the paragraph may have AbstractStyle, but we need Font objects + wrapped_words = [] + 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) + + # Create a new Word with the cell's Font + new_word = AbstractWord(word_text, font) + wrapped_words.append(new_word) + # Layout words using Line objects with wrapping word_index = 0 pretext = None - while word_index < len(words): + while word_index < len(wrapped_words): # Check if we have space for another line if current_y + ascent + descent > y + available_height: break # No more space in cell @@ -172,29 +191,29 @@ class TableCellRenderer(Box): # 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 + while word_index < len(wrapped_words): + word = wrapped_words[word_index] # Try to add word to line - success, overflow = line.add_word(word_obj, pretext) + success, overflow = line.add_word(word, pretext) pretext = None # Clear pretext after use if success: line_has_content = True if overflow: # Word was hyphenated, carry over to next line + # DON'T increment word_index - we need to add the overflow + # to the next line with the same word pretext = overflow - word_index += 1 + break # Move to next line + else: + # Word fit completely, move to next word + 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 + # Even first word doesn't fit, force it anyway and advance + # This prevents infinite loops with words that truly can't fit word_index += 1 break @@ -454,11 +473,16 @@ class TableRenderer(Box): column_widths = [column_width] * num_columns # Calculate row heights - header_height = 35 if any(1 for section, + # 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 # Check if any body rows contain images - if so, use larger height - body_height = 30 + 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(): @@ -468,7 +492,7 @@ class TableRenderer(Box): body_height = max(body_height, 120) break - footer_height = 30 if any(1 for section, + footer_height = 60 if any(1 for section, _ in all_rows if section == "footer") else 0 row_heights = {