@@ -34,8 +34,6 @@ from pyWebLayout.concrete.page import Container, Page
|
||||
# Abstract components
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
|
||||
# Layout components
|
||||
from pyWebLayout.table import Table, TableCell
|
||||
|
||||
# IO functionality (reading and writing)
|
||||
from pyWebLayout.io import (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from enum import Enum
|
||||
from typing import Callable, Dict, Any, Optional, Union, List, Tuple
|
||||
from pyWebLayout.base import Interactable
|
||||
from pyWebLayout.core.base import Interactable
|
||||
|
||||
|
||||
class LinkType(Enum):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from pyWebLayout.base import Queriable
|
||||
from pyWebLayout.core.base import Queriable
|
||||
from pyWebLayout.style import Font
|
||||
from typing import Tuple, Union, List, Optional, Dict
|
||||
import pyphen
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
|
||||
from abc import ABC
|
||||
import numpy as np
|
||||
|
||||
from pyWebLayout.style import Alignment
|
||||
|
||||
|
||||
class Renderable(ABC):
|
||||
"""
|
||||
Abstract base class for any object that can be rendered to an image.
|
||||
All renderable objects must implement the render method.
|
||||
"""
|
||||
def render(self):
|
||||
"""
|
||||
Render the object to an image.
|
||||
|
||||
Returns:
|
||||
PIL.Image: The rendered image
|
||||
"""
|
||||
pass
|
||||
|
||||
class Interactable(ABC):
|
||||
"""
|
||||
Abstract base class for any object that can be interacted with.
|
||||
Interactable objects must have a callback that is executed when interacted with.
|
||||
"""
|
||||
def __init__(self, callback=None):
|
||||
"""
|
||||
Initialize an interactable object.
|
||||
|
||||
Args:
|
||||
callback: The function to call when this object is interacted with
|
||||
"""
|
||||
self._callback = callback
|
||||
|
||||
def interact(self, point: np.generic):
|
||||
"""
|
||||
Handle interaction at the given point.
|
||||
|
||||
Args:
|
||||
point: The coordinates of the interaction
|
||||
|
||||
Returns:
|
||||
The result of calling the callback function with the point
|
||||
"""
|
||||
if self._callback is None:
|
||||
return None
|
||||
return self._callback(point)
|
||||
|
||||
class Layoutable(ABC):
|
||||
"""
|
||||
Abstract base class for any object that can be laid out.
|
||||
Layoutable objects must implement the layout method which arranges their contents.
|
||||
"""
|
||||
def layout(self):
|
||||
"""
|
||||
Layout the object's contents.
|
||||
This method should be called before rendering to properly arrange the object's contents.
|
||||
"""
|
||||
pass
|
||||
|
||||
class Queriable(ABC):
|
||||
|
||||
def in_object(self, point:np.generic):
|
||||
"""
|
||||
check if a point is in the object
|
||||
"""
|
||||
pass
|
||||
@@ -1,8 +1,8 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from pyWebLayout.base import Renderable, Queriable
|
||||
from pyWebLayout.layout import Alignment
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
class Box(Renderable, Queriable):
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional, Dict, Any, Tuple, List, Union
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from pyWebLayout.base import Renderable, Queriable
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
from pyWebLayout.abstract.functional import Link, Button, Form, FormField, LinkType, FormFieldType
|
||||
from pyWebLayout.style import Font, TextDecoration
|
||||
from .box import Box
|
||||
|
||||
@@ -3,10 +3,10 @@ from typing import Optional, Tuple, Union, Dict, Any
|
||||
import numpy as np
|
||||
from PIL import Image as PILImage, ImageDraw, ImageFont
|
||||
|
||||
from pyWebLayout.base import Renderable, Queriable
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
from pyWebLayout.abstract.block import Image as AbstractImage
|
||||
from .box import Box
|
||||
from pyWebLayout.layout import Alignment
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class RenderableImage(Box, Queriable):
|
||||
|
||||
@@ -2,9 +2,9 @@ from typing import List, Tuple, Optional, Dict, Any
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
from pyWebLayout.base import Renderable, Layoutable
|
||||
from pyWebLayout.core.base import Renderable, Layoutable
|
||||
from .box import Box
|
||||
from pyWebLayout.layout import Alignment
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
|
||||
|
||||
class Container(Box, Layoutable):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from pyWebLayout.base import Renderable, Queriable
|
||||
from pyWebLayout.core.base import Renderable, Queriable
|
||||
from .box import Box
|
||||
from pyWebLayout.layout import Alignment
|
||||
from pyWebLayout.style.layout import Alignment
|
||||
from pyWebLayout.style import Font, FontStyle, FontWeight, TextDecoration
|
||||
from pyWebLayout.abstract.inline import Word
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
@@ -1,919 +0,0 @@
|
||||
import re
|
||||
from html.parser import HTMLParser as BaseHTMLParser
|
||||
from typing import Dict, List, Optional, Tuple, Union, Any, Set, Callable
|
||||
import urllib.parse
|
||||
from PIL import Image
|
||||
|
||||
from .style import Font, FontStyle, FontWeight, TextDecoration
|
||||
from .abstract.document import Document, MetadataType, Book, Chapter
|
||||
from .abstract.block import (
|
||||
Block, BlockType, Paragraph, Heading, HeadingLevel, Quote, CodeBlock,
|
||||
HList, ListStyle, ListItem, Table, TableRow, TableCell, HorizontalRule
|
||||
)
|
||||
from .abstract.inline import Word, FormattedSpan, LineBreak
|
||||
from .abstract.functional import Link, LinkType, Button, Form, FormField, FormFieldType
|
||||
from .concrete.page import Page
|
||||
from pyWebLayout.layout import Alignment
|
||||
|
||||
|
||||
class HTMLParser(BaseHTMLParser):
|
||||
"""
|
||||
HTML parser that builds an abstract document representation from HTML content.
|
||||
This parser converts HTML to abstract document classes without any rendering specifics.
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: Optional[str] = None):
|
||||
"""
|
||||
Initialize the HTML parser.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for resolving relative links
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# Document structure
|
||||
self.document = Document()
|
||||
|
||||
# State variables
|
||||
self._current_block = None
|
||||
self._block_stack: List[Block] = []
|
||||
|
||||
# Text handling
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
self._text_buffer = ""
|
||||
|
||||
# Style state
|
||||
self._style_stack: List[Dict[str, Any]] = []
|
||||
self._current_style = {
|
||||
'font_size': 12,
|
||||
'font_weight': FontWeight.NORMAL,
|
||||
'font_style': FontStyle.NORMAL,
|
||||
'decoration': TextDecoration.NONE,
|
||||
'color': (0, 0, 0),
|
||||
'background': None,
|
||||
'language': 'en_US'
|
||||
}
|
||||
|
||||
# Tag state
|
||||
self._list_stack: List[HList] = []
|
||||
self._table_stack: List[Table] = []
|
||||
self._current_table_row = None
|
||||
|
||||
# Link handling
|
||||
self._base_url = base_url
|
||||
self._in_link = False
|
||||
self._current_link = None
|
||||
|
||||
# Special state flags
|
||||
self._in_head = False
|
||||
self._in_title = False
|
||||
self._in_script = False
|
||||
self._in_style = False
|
||||
self._script_buffer = ""
|
||||
self._style_buffer = ""
|
||||
self._title_buffer = ""
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]):
|
||||
"""
|
||||
Handle the start of an HTML tag.
|
||||
|
||||
Args:
|
||||
tag: The tag name
|
||||
attrs: List of attribute tuples (name, value)
|
||||
"""
|
||||
tag = tag.lower()
|
||||
attrs_dict = dict(attrs)
|
||||
|
||||
# Special handling for elements where we collect content
|
||||
if self._in_script and tag != 'script':
|
||||
return
|
||||
if self._in_style and tag != 'style':
|
||||
return
|
||||
|
||||
# Parse style attribute if present
|
||||
style = {}
|
||||
if 'style' in attrs_dict:
|
||||
style = self._parse_style(attrs_dict['style'])
|
||||
|
||||
# Apply tag-specific styling based on the tag
|
||||
tag_style = self._get_tag_style(tag)
|
||||
for key, value in tag_style.items():
|
||||
if key not in style:
|
||||
style[key] = value
|
||||
|
||||
# Push the current style and apply the new style
|
||||
self._push_style(style)
|
||||
|
||||
# Handle specific tags
|
||||
if tag == 'html':
|
||||
# Set document language if specified
|
||||
if 'lang' in attrs_dict:
|
||||
self.document.set_metadata(MetadataType.LANGUAGE, attrs_dict['lang'])
|
||||
|
||||
elif tag == 'head':
|
||||
self._in_head = True
|
||||
|
||||
elif tag == 'title' and self._in_head:
|
||||
self._in_title = True
|
||||
self._title_buffer = ""
|
||||
|
||||
elif tag == 'meta' and self._in_head:
|
||||
self._handle_meta_tag(attrs_dict)
|
||||
|
||||
elif tag == 'link' and self._in_head:
|
||||
self._handle_link_tag(attrs_dict)
|
||||
|
||||
elif tag == 'script':
|
||||
self._in_script = True
|
||||
self._script_buffer = ""
|
||||
|
||||
elif tag == 'style':
|
||||
self._in_style = True
|
||||
self._style_buffer = ""
|
||||
|
||||
elif tag == 'body':
|
||||
# Body attributes can contain style information
|
||||
pass
|
||||
|
||||
elif tag == 'p':
|
||||
self._flush_text() # Flush any pending text
|
||||
self._current_paragraph = Paragraph()
|
||||
|
||||
# Add the paragraph to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(self._current_paragraph)
|
||||
else:
|
||||
self.document.add_block(self._current_paragraph)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(self._current_paragraph)
|
||||
self._current_block = self._current_paragraph
|
||||
|
||||
elif tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'):
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Determine heading level
|
||||
level_map = {
|
||||
'h1': HeadingLevel.H1,
|
||||
'h2': HeadingLevel.H2,
|
||||
'h3': HeadingLevel.H3,
|
||||
'h4': HeadingLevel.H4,
|
||||
'h5': HeadingLevel.H5,
|
||||
'h6': HeadingLevel.H6
|
||||
}
|
||||
|
||||
heading = Heading(level=level_map[tag])
|
||||
|
||||
# Add the heading to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(heading)
|
||||
else:
|
||||
self.document.add_block(heading)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(heading)
|
||||
self._current_block = heading
|
||||
self._current_paragraph = heading # Heading inherits from Paragraph
|
||||
|
||||
elif tag == 'div':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# For divs, we create a new paragraph as a container
|
||||
div_para = Paragraph()
|
||||
|
||||
# Add the div to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(div_para)
|
||||
else:
|
||||
self.document.add_block(div_para)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(div_para)
|
||||
self._current_block = div_para
|
||||
self._current_paragraph = div_para
|
||||
|
||||
elif tag == 'blockquote':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
quote = Quote()
|
||||
|
||||
# Add the quote to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(quote)
|
||||
else:
|
||||
self.document.add_block(quote)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(quote)
|
||||
self._current_block = quote
|
||||
|
||||
elif tag == 'pre':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Pre can optionally contain a code block
|
||||
# We'll create a paragraph for now, and if we find a code tag inside,
|
||||
# we'll replace it with a code block
|
||||
pre_para = Paragraph()
|
||||
|
||||
# Add the pre to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(pre_para)
|
||||
else:
|
||||
self.document.add_block(pre_para)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(pre_para)
|
||||
self._current_block = pre_para
|
||||
self._current_paragraph = pre_para
|
||||
|
||||
elif tag == 'code':
|
||||
# If we're inside a pre, replace the paragraph with a code block
|
||||
if self._block_stack and isinstance(self._block_stack[-1], Paragraph):
|
||||
pre_para = self._block_stack.pop()
|
||||
|
||||
# Get the language from class if specified (e.g., class="language-python")
|
||||
language = ""
|
||||
if 'class' in attrs_dict:
|
||||
class_attr = attrs_dict['class']
|
||||
if class_attr.startswith('language-'):
|
||||
language = class_attr[9:]
|
||||
|
||||
code_block = CodeBlock(language=language)
|
||||
|
||||
# Replace the paragraph with the code block
|
||||
if pre_para.parent:
|
||||
parent = pre_para.parent
|
||||
if hasattr(parent, '_blocks'):
|
||||
# Find the paragraph in the parent's blocks and replace it
|
||||
for i, block in enumerate(parent._blocks):
|
||||
if block == pre_para:
|
||||
parent._blocks[i] = code_block
|
||||
break
|
||||
|
||||
# Push the code block to the stack
|
||||
self._block_stack.append(code_block)
|
||||
self._current_block = code_block
|
||||
self._current_paragraph = None
|
||||
else:
|
||||
# If not in a pre, just create a formatted span for code
|
||||
self._current_span = None # Force creation of a new span with code style
|
||||
|
||||
elif tag in ('ul', 'ol', 'dl'):
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Determine list style
|
||||
style_map = {
|
||||
'ul': ListStyle.UNORDERED,
|
||||
'ol': ListStyle.ORDERED,
|
||||
'dl': ListStyle.DEFINITION
|
||||
}
|
||||
|
||||
list_block = HList(style=style_map[tag])
|
||||
|
||||
# Add the list to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(list_block)
|
||||
else:
|
||||
self.document.add_block(list_block)
|
||||
|
||||
# Push to block stack and list stack
|
||||
self._block_stack.append(list_block)
|
||||
self._list_stack.append(list_block)
|
||||
self._current_block = list_block
|
||||
self._current_paragraph = None
|
||||
|
||||
elif tag == 'li' and self._list_stack:
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
list_item = ListItem()
|
||||
|
||||
# Add to the current list
|
||||
current_list = self._list_stack[-1]
|
||||
current_list.add_item(list_item)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(list_item)
|
||||
self._current_block = list_item
|
||||
self._current_paragraph = None
|
||||
|
||||
elif tag == 'dt' and self._list_stack and self._list_stack[-1].style == ListStyle.DEFINITION:
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# For definition term, we create a list item with a term
|
||||
list_item = ListItem(term="") # Will be filled by content
|
||||
|
||||
# Add to the current list
|
||||
current_list = self._list_stack[-1]
|
||||
current_list.add_item(list_item)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(list_item)
|
||||
self._current_block = list_item
|
||||
|
||||
# Create a paragraph for the term content
|
||||
term_para = Paragraph()
|
||||
list_item.add_block(term_para)
|
||||
self._current_paragraph = term_para
|
||||
|
||||
elif tag == 'dd' and self._list_stack and self._list_stack[-1].style == ListStyle.DEFINITION:
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Find the last dt item
|
||||
current_list = self._list_stack[-1]
|
||||
if current_list._items:
|
||||
list_item = current_list._items[-1]
|
||||
|
||||
# Create a paragraph for the description content
|
||||
desc_para = Paragraph()
|
||||
list_item.add_block(desc_para)
|
||||
|
||||
# Update current state
|
||||
self._current_paragraph = desc_para
|
||||
else:
|
||||
# If no dt found, create a new list item
|
||||
list_item = ListItem()
|
||||
current_list.add_item(list_item)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(list_item)
|
||||
self._current_block = list_item
|
||||
|
||||
# Create a paragraph for the description content
|
||||
desc_para = Paragraph()
|
||||
list_item.add_block(desc_para)
|
||||
self._current_paragraph = desc_para
|
||||
|
||||
elif tag == 'table':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Create a new table
|
||||
caption = None
|
||||
if 'summary' in attrs_dict:
|
||||
caption = attrs_dict['summary']
|
||||
|
||||
table = Table(caption=caption)
|
||||
|
||||
# Add the table to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(table)
|
||||
else:
|
||||
self.document.add_block(table)
|
||||
|
||||
# Push to block stack and table stack
|
||||
self._block_stack.append(table)
|
||||
self._table_stack.append(table)
|
||||
self._current_block = table
|
||||
self._current_paragraph = None
|
||||
|
||||
elif tag in ('thead', 'tbody', 'tfoot') and self._table_stack:
|
||||
# Just track the current section - no need to create new objects
|
||||
self._current_table_section = tag
|
||||
|
||||
elif tag == 'tr' and self._table_stack:
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Create a new row
|
||||
row = TableRow()
|
||||
|
||||
# Add to the current table
|
||||
current_table = self._table_stack[-1]
|
||||
|
||||
# Determine the section based on context
|
||||
section = "body"
|
||||
if hasattr(self, '_current_table_section'):
|
||||
if self._current_table_section == 'thead':
|
||||
section = "header"
|
||||
elif self._current_table_section == 'tfoot':
|
||||
section = "footer"
|
||||
|
||||
current_table.add_row(row, section=section)
|
||||
|
||||
# Update state
|
||||
self._current_table_row = row
|
||||
self._current_paragraph = None
|
||||
|
||||
elif tag in ('td', 'th') and self._current_table_row:
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Parse attributes
|
||||
colspan = 1
|
||||
rowspan = 1
|
||||
|
||||
if 'colspan' in attrs_dict:
|
||||
try:
|
||||
colspan = int(attrs_dict['colspan'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if 'rowspan' in attrs_dict:
|
||||
try:
|
||||
rowspan = int(attrs_dict['rowspan'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Create a new cell
|
||||
is_header = (tag == 'th')
|
||||
cell = TableCell(is_header=is_header, colspan=colspan, rowspan=rowspan)
|
||||
|
||||
# Add to the current row
|
||||
self._current_table_row.add_cell(cell)
|
||||
|
||||
# Push to block stack
|
||||
self._block_stack.append(cell)
|
||||
self._current_block = cell
|
||||
|
||||
# Create a paragraph for the cell content
|
||||
cell_para = Paragraph()
|
||||
cell.add_block(cell_para)
|
||||
self._current_paragraph = cell_para
|
||||
|
||||
elif tag == 'a':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Parse attributes
|
||||
href = attrs_dict.get('href', '')
|
||||
title = attrs_dict.get('title', '')
|
||||
|
||||
# Determine link type
|
||||
link_type = LinkType.INTERNAL
|
||||
if href.startswith('http://') or href.startswith('https://'):
|
||||
link_type = LinkType.EXTERNAL
|
||||
elif href.startswith('javascript:'):
|
||||
link_type = LinkType.FUNCTION
|
||||
elif href.startswith('api:'):
|
||||
link_type = LinkType.API
|
||||
href = href[4:] # Remove api: prefix
|
||||
|
||||
# If we have a base URL and the href is relative, resolve it
|
||||
if self._base_url and not href.startswith(('http://', 'https://', 'javascript:', 'api:', '#')):
|
||||
href = urllib.parse.urljoin(self._base_url, href)
|
||||
|
||||
# Create a Link object
|
||||
self._current_link = Link(
|
||||
location=href,
|
||||
link_type=link_type,
|
||||
title=title if title else None
|
||||
)
|
||||
|
||||
# Set the flag to indicate we're inside a link
|
||||
self._in_link = True
|
||||
|
||||
# Force creation of a new span with link style
|
||||
self._current_span = None
|
||||
|
||||
elif tag == 'img':
|
||||
# Handle image
|
||||
src = attrs_dict.get('src', '')
|
||||
alt = attrs_dict.get('alt', '')
|
||||
|
||||
# Parse width and height if provided
|
||||
width = None
|
||||
height = None
|
||||
if 'width' in attrs_dict:
|
||||
try:
|
||||
width = int(attrs_dict['width'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if 'height' in attrs_dict:
|
||||
try:
|
||||
height = int(attrs_dict['height'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# If we have a base URL and the src is relative, resolve it
|
||||
if self._base_url and not src.startswith(('http://', 'https://')):
|
||||
src = urllib.parse.urljoin(self._base_url, src)
|
||||
|
||||
# Create an Image block
|
||||
from .abstract.block import Image
|
||||
image = Image(source=src, alt_text=alt, width=width, height=height)
|
||||
|
||||
# Add the image to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(image)
|
||||
else:
|
||||
self.document.add_block(image)
|
||||
|
||||
# Also add as a resource for backwards compatibility
|
||||
resource_name = f"img_{len(self.document._resources) + 1}"
|
||||
self.document.add_resource(resource_name, {
|
||||
'type': 'image',
|
||||
'src': src,
|
||||
'alt': alt,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'image_object': image
|
||||
})
|
||||
|
||||
elif tag == 'br':
|
||||
|
||||
# Add a line break
|
||||
if self._current_paragraph:
|
||||
line_break = LineBreak()
|
||||
if hasattr(self._current_paragraph, 'add_block'):
|
||||
self._current_paragraph.add_block(line_break)
|
||||
|
||||
# Flush any text before the break
|
||||
self._flush_text()
|
||||
|
||||
elif tag == 'hr':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Create a horizontal rule
|
||||
hr = HorizontalRule()
|
||||
|
||||
# Add to the current block or document
|
||||
if self._current_block and hasattr(self._current_block, 'add_block'):
|
||||
self._current_block.add_block(hr)
|
||||
else:
|
||||
self.document.add_block(hr)
|
||||
|
||||
elif tag in ('b', 'strong'):
|
||||
# Bold text
|
||||
self._current_style['font_weight'] = FontWeight.BOLD
|
||||
self._current_span = None # Force creation of a new span
|
||||
|
||||
elif tag in ('i', 'em'):
|
||||
# Italic text
|
||||
self._current_style['font_style'] = FontStyle.ITALIC
|
||||
self._current_span = None # Force creation of a new span
|
||||
|
||||
elif tag == 'u':
|
||||
# Underlined text
|
||||
self._current_style['decoration'] = TextDecoration.UNDERLINE
|
||||
self._current_span = None # Force creation of a new span
|
||||
|
||||
elif tag == 'span':
|
||||
# Span can have style attributes
|
||||
self._current_span = None # Force creation of a new span
|
||||
|
||||
elif tag == 'form':
|
||||
self._flush_text() # Flush any pending text
|
||||
|
||||
# Parse attributes
|
||||
form_id = attrs_dict.get('id', f"form_{len(self.document._resources) + 1}")
|
||||
action = attrs_dict.get('action', '')
|
||||
|
||||
# Create a Form object
|
||||
form = Form(form_id=form_id, action=action)
|
||||
|
||||
# Add as a resource
|
||||
self.document.add_resource(form_id, form)
|
||||
|
||||
# TODO: Create a proper Form block class and add it to the document
|
||||
|
||||
elif tag == 'input':
|
||||
# Parse attributes
|
||||
input_type = attrs_dict.get('type', 'text')
|
||||
input_name = attrs_dict.get('name', '')
|
||||
input_value = attrs_dict.get('value', '')
|
||||
input_required = 'required' in attrs_dict
|
||||
|
||||
# Map HTML input types to FormFieldType
|
||||
type_map = {
|
||||
'text': FormFieldType.TEXT,
|
||||
'password': FormFieldType.PASSWORD,
|
||||
'checkbox': FormFieldType.CHECKBOX,
|
||||
'radio': FormFieldType.RADIO,
|
||||
'number': FormFieldType.NUMBER,
|
||||
'date': FormFieldType.DATE,
|
||||
'time': FormFieldType.TIME,
|
||||
'email': FormFieldType.EMAIL,
|
||||
'url': FormFieldType.URL,
|
||||
'color': FormFieldType.COLOR,
|
||||
'range': FormFieldType.RANGE,
|
||||
'hidden': FormFieldType.HIDDEN
|
||||
}
|
||||
|
||||
field_type = type_map.get(input_type, FormFieldType.TEXT)
|
||||
|
||||
# Create a FormField object
|
||||
field = FormField(
|
||||
name=input_name,
|
||||
field_type=field_type,
|
||||
label=attrs_dict.get('placeholder', input_name),
|
||||
value=input_value,
|
||||
required=input_required
|
||||
)
|
||||
|
||||
# TODO: Add the field to a form if inside a form
|
||||
|
||||
elif tag == 'textarea':
|
||||
# Similar to input but with multiline content
|
||||
# We'll handle the content in handle_data
|
||||
pass
|
||||
|
||||
elif tag == 'select':
|
||||
# Similar to input but with options
|
||||
# We'll handle the options in handle_data
|
||||
pass
|
||||
|
||||
elif tag == 'button':
|
||||
# Parse attributes
|
||||
button_type = attrs_dict.get('type', 'button')
|
||||
button_name = attrs_dict.get('name', '')
|
||||
|
||||
# TODO: Create a Button object and add it to the document
|
||||
|
||||
def handle_endtag(self, tag: str):
|
||||
"""
|
||||
Handle the end of an HTML tag.
|
||||
|
||||
Args:
|
||||
tag: The tag name
|
||||
"""
|
||||
tag = tag.lower()
|
||||
|
||||
# Special handling for elements where we collect content
|
||||
if tag == 'script' and self._in_script:
|
||||
self._in_script = False
|
||||
self.document.add_script(self._script_buffer)
|
||||
self._script_buffer = ""
|
||||
self._pop_style()
|
||||
return
|
||||
|
||||
if tag == 'style' and self._in_style:
|
||||
self._in_style = False
|
||||
# Parse the style and add to document
|
||||
stylesheet = self._parse_css(self._style_buffer)
|
||||
if stylesheet:
|
||||
self.document.add_stylesheet(stylesheet)
|
||||
self._style_buffer = ""
|
||||
self._pop_style()
|
||||
return
|
||||
|
||||
if tag == 'title' and self._in_title:
|
||||
self._in_title = False
|
||||
self.document.set_title(self._title_buffer.strip())
|
||||
self._title_buffer = ""
|
||||
self._pop_style()
|
||||
return
|
||||
|
||||
if self._in_script and tag != 'script':
|
||||
return
|
||||
if self._in_style and tag != 'style':
|
||||
return
|
||||
|
||||
# Flush any accumulated text
|
||||
self._flush_text()
|
||||
|
||||
# Handle specific end tags
|
||||
if tag == 'head':
|
||||
self._in_head = False
|
||||
|
||||
elif tag == 'body':
|
||||
pass # Nothing special to do
|
||||
|
||||
elif tag in ('p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'):
|
||||
# Pop from block stack
|
||||
if self._block_stack:
|
||||
self._block_stack.pop()
|
||||
|
||||
# Update current block
|
||||
if self._block_stack:
|
||||
self._current_block = self._block_stack[-1]
|
||||
else:
|
||||
self._current_block = None
|
||||
|
||||
# Reset current paragraph
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
|
||||
elif tag == 'code':
|
||||
# If we're inside a code block, no need to do anything special
|
||||
pass
|
||||
|
||||
elif tag in ('ul', 'ol', 'dl'):
|
||||
# Pop from block stack and list stack
|
||||
if self._block_stack:
|
||||
self._block_stack.pop()
|
||||
|
||||
if self._list_stack:
|
||||
self._list_stack.pop()
|
||||
|
||||
# Update current block
|
||||
if self._block_stack:
|
||||
self._current_block = self._block_stack[-1]
|
||||
else:
|
||||
self._current_block = None
|
||||
|
||||
# Reset current paragraph
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
|
||||
elif tag in ('li', 'dt', 'dd'):
|
||||
# Pop from block stack
|
||||
if self._block_stack:
|
||||
self._block_stack.pop()
|
||||
|
||||
# Update current block
|
||||
if self._block_stack:
|
||||
self._current_block = self._block_stack[-1]
|
||||
else:
|
||||
self._current_block = None
|
||||
|
||||
# Reset current paragraph
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
|
||||
elif tag == 'table':
|
||||
# Pop from block stack and table stack
|
||||
if self._block_stack:
|
||||
self._block_stack.pop()
|
||||
|
||||
if self._table_stack:
|
||||
self._table_stack.pop()
|
||||
|
||||
# Update current block
|
||||
if self._block_stack:
|
||||
self._current_block = self._block_stack[-1]
|
||||
else:
|
||||
self._current_block = None
|
||||
|
||||
# Reset current paragraph and table state
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
self._current_table_row = None
|
||||
if hasattr(self, '_current_table_section'):
|
||||
delattr(self, '_current_table_section')
|
||||
|
||||
elif tag in ('thead', 'tbody', 'tfoot'):
|
||||
# Clear current section
|
||||
if hasattr(self, '_current_table_section'):
|
||||
delattr(self, '_current_table_section')
|
||||
|
||||
elif tag == 'tr':
|
||||
# Reset current row
|
||||
self._current_table_row = None
|
||||
|
||||
elif tag in ('td', 'th'):
|
||||
# Pop from block stack
|
||||
if self._block_stack:
|
||||
self._block_stack.pop()
|
||||
|
||||
# Update current block
|
||||
if self._block_stack:
|
||||
self._current_block = self._block_stack[-1]
|
||||
else:
|
||||
self._current_block = None
|
||||
|
||||
# Reset current paragraph
|
||||
self._current_paragraph = None
|
||||
self._current_span = None
|
||||
|
||||
elif tag == 'a':
|
||||
# End of link
|
||||
self._in_link = False
|
||||
self._current_link = None
|
||||
|
||||
elif tag in ('b', 'strong', 'i', 'em', 'u', 'span'):
|
||||
# End of styled text
|
||||
self._current_span = None
|
||||
|
||||
# Pop style regardless of tag
|
||||
self._pop_style()
|
||||
|
||||
def handle_data(self, data: str):
|
||||
"""
|
||||
Handle text data.
|
||||
|
||||
Args:
|
||||
data: The text data
|
||||
"""
|
||||
if self._in_script:
|
||||
self._script_buffer += data
|
||||
return
|
||||
|
||||
if self._in_style:
|
||||
self._style_buffer += data
|
||||
return
|
||||
|
||||
if self._in_title:
|
||||
self._title_buffer += data
|
||||
return
|
||||
|
||||
# Add to text buffer
|
||||
self._text_buffer += data
|
||||
|
||||
def handle_entityref(self, name: str):
|
||||
"""
|
||||
Handle an HTML entity reference.
|
||||
|
||||
Args:
|
||||
name: The entity name
|
||||
"""
|
||||
# Map common entity references to characters
|
||||
entities = {
|
||||
'lt': '<',
|
||||
'gt': '>',
|
||||
'amp': '&',
|
||||
'quot': '"',
|
||||
'apos': "'",
|
||||
'nbsp': ' ',
|
||||
'copy': '©',
|
||||
'reg': '®',
|
||||
'trade': '™',
|
||||
}
|
||||
|
||||
if name in entities:
|
||||
char = entities[name]
|
||||
else:
|
||||
try:
|
||||
import html.entities
|
||||
char = chr(html.entities.name2codepoint[name])
|
||||
except (KeyError, ImportError):
|
||||
char = f'&{name};'
|
||||
|
||||
# Handle based on context
|
||||
if self._in_script:
|
||||
self._script_buffer += char
|
||||
elif self._in_style:
|
||||
self._style_buffer += char
|
||||
elif self._in_title:
|
||||
self._title_buffer += char
|
||||
else:
|
||||
self._text_buffer += char
|
||||
|
||||
def handle_charref(self, name: str):
|
||||
"""
|
||||
Handle a character reference.
|
||||
|
||||
Args:
|
||||
name: The character reference (decimal or hex)
|
||||
"""
|
||||
# Convert character reference to character
|
||||
if name.startswith('x'):
|
||||
# Hexadecimal reference
|
||||
char = chr(int(name[1:], 16))
|
||||
else:
|
||||
# Decimal reference
|
||||
char = chr(int(name))
|
||||
|
||||
# Handle based on context
|
||||
if self._in_script:
|
||||
self._script_buffer += char
|
||||
elif self._in_style:
|
||||
self._style_buffer += char
|
||||
elif self._in_title:
|
||||
self._title_buffer += char
|
||||
else:
|
||||
self._text_buffer += char
|
||||
|
||||
def _push_style(self, style: Dict[str, Any]):
|
||||
"""
|
||||
Push a new style onto the style stack.
|
||||
|
||||
Args:
|
||||
style: The style to push
|
||||
"""
|
||||
# Save the current style
|
||||
self._style_stack.append(self._current_style.copy())
|
||||
|
||||
# Apply the new style
|
||||
for key, value in style.items():
|
||||
self._current_style[key] = value
|
||||
|
||||
def _pop_style(self):
|
||||
"""Pop a style from the style stack."""
|
||||
if self._style_stack:
|
||||
self._current_style = self._style_stack.pop()
|
||||
|
||||
def _get_tag_style(self, tag: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the default style for a tag.
|
||||
|
||||
Args:
|
||||
tag: The tag name
|
||||
|
||||
Returns:
|
||||
A dictionary of style properties
|
||||
"""
|
||||
# Default styles for common tags
|
||||
tag_styles = {
|
||||
'h1': {'font_size': 24, 'font_weight': FontWeight.BOLD},
|
||||
'h2': {'font_size': 20, 'font_weight': FontWeight.BOLD},
|
||||
'h3': {'font_size': 18, 'font_weight': FontWeight.BOLD},
|
||||
'h4': {'font_size': 16, 'font_weight': FontWeight.BOLD},
|
||||
'h5': {'font_size': 14, 'font_weight': FontWeight.BOLD},
|
||||
'h6': {'font_size': 12, 'font_weight': FontWeight.BOLD},
|
||||
'b': {'font_weight': FontWeight.BOLD},
|
||||
'strong': {'font_weight': FontWeight.BOLD},
|
||||
'i': {'font_style': FontStyle.ITALIC},
|
||||
'em': {'font_style': FontStyle.ITALIC},
|
||||
'u': {'decoration': TextDecoration.UNDERLINE},
|
||||
'a': {'decoration': TextDecoration.UNDERLINE, 'color': (0, 0, 255)},
|
||||
'code': {'font_family': 'monospace', 'background': (240, 240, 240, 255)},
|
||||
'pre': {'font_family': 'monospace'},
|
||||
}
|
||||
|
||||
return tag_styles.get(tag, {})
|
||||
|
||||
def _create_font(self) -> Font:
|
||||
"""
|
||||
Create a Font object from the current style.
|
||||
|
||||
Returns:
|
||||
Font: A font object with the current style settings
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
## list langauges
|
||||
@@ -1,176 +0,0 @@
|
||||
# this should contain classes for how different object can be rendered, e.g. bold, italic, regular
|
||||
from PIL import ImageFont
|
||||
from enum import Enum
|
||||
from typing import Tuple, Union, Optional
|
||||
|
||||
|
||||
class FontWeight(Enum):
|
||||
NORMAL = "normal"
|
||||
BOLD = "bold"
|
||||
|
||||
|
||||
class FontStyle(Enum):
|
||||
NORMAL = "normal"
|
||||
ITALIC = "italic"
|
||||
|
||||
|
||||
class TextDecoration(Enum):
|
||||
NONE = "none"
|
||||
UNDERLINE = "underline"
|
||||
STRIKETHROUGH = "strikethrough"
|
||||
|
||||
|
||||
class Font:
|
||||
"""
|
||||
Font class to manage text rendering properties including font face, size, color, and styling.
|
||||
This class is used by the text renderer to determine how to render text.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
font_path: Optional[str] = None,
|
||||
font_size: int = 12,
|
||||
colour: Tuple[int, int, int] = (0, 0, 0),
|
||||
weight: FontWeight = FontWeight.NORMAL,
|
||||
style: FontStyle = FontStyle.NORMAL,
|
||||
decoration: TextDecoration = TextDecoration.NONE,
|
||||
background: Optional[Tuple[int, int, int, int]] = None,
|
||||
language = "en_EN"):
|
||||
"""
|
||||
Initialize a Font object with the specified properties.
|
||||
|
||||
Args:
|
||||
font_path: Path to the font file (.ttf, .otf). If None, uses default font.
|
||||
font_size: Size of the font in points.
|
||||
colour: RGB color tuple for the text.
|
||||
weight: Font weight (normal or bold).
|
||||
style: Font style (normal or italic).
|
||||
decoration: Text decoration (none, underline, or strikethrough).
|
||||
background: RGBA background color for the text. If None, transparent background.
|
||||
"""
|
||||
self._font_path = font_path
|
||||
self._font_size = font_size
|
||||
self._colour = colour
|
||||
self._weight = weight
|
||||
self._style = style
|
||||
self._decoration = decoration
|
||||
self._background = background if background else (255, 255, 255, 0)
|
||||
self.language = language
|
||||
# Load the font file or use default
|
||||
self._load_font()
|
||||
|
||||
def _load_font(self):
|
||||
"""Load the font using PIL's ImageFont"""
|
||||
try:
|
||||
if self._font_path:
|
||||
self._font = ImageFont.truetype(
|
||||
self._font_path,
|
||||
self._font_size
|
||||
)
|
||||
else:
|
||||
# Use default font
|
||||
self._font = ImageFont.load_default()
|
||||
if self._font_size != 12: # Default size might not be 12
|
||||
self._font = ImageFont.truetype(self._font.path, self._font_size)
|
||||
except Exception as e:
|
||||
print(f"Error loading font: {e}")
|
||||
self._font = ImageFont.load_default()
|
||||
|
||||
@property
|
||||
def font(self):
|
||||
"""Get the PIL ImageFont object"""
|
||||
return self._font
|
||||
|
||||
@property
|
||||
def font_size(self):
|
||||
"""Get the font size"""
|
||||
return self._font_size
|
||||
|
||||
@property
|
||||
def colour(self):
|
||||
"""Get the text color"""
|
||||
return self._colour
|
||||
|
||||
@property
|
||||
def color(self):
|
||||
"""Alias for colour (American spelling)"""
|
||||
return self._colour
|
||||
|
||||
@property
|
||||
def background(self):
|
||||
"""Get the background color"""
|
||||
return self._background
|
||||
|
||||
@property
|
||||
def weight(self):
|
||||
"""Get the font weight"""
|
||||
return self._weight
|
||||
|
||||
@property
|
||||
def style(self):
|
||||
"""Get the font style"""
|
||||
return self._style
|
||||
|
||||
@property
|
||||
def decoration(self):
|
||||
"""Get the text decoration"""
|
||||
return self._decoration
|
||||
|
||||
def with_size(self, size: int):
|
||||
"""Create a new Font object with modified size"""
|
||||
return Font(
|
||||
self._font_path,
|
||||
size,
|
||||
self._colour,
|
||||
self._weight,
|
||||
self._style,
|
||||
self._decoration,
|
||||
self._background
|
||||
)
|
||||
|
||||
def with_colour(self, colour: Tuple[int, int, int]):
|
||||
"""Create a new Font object with modified colour"""
|
||||
return Font(
|
||||
self._font_path,
|
||||
self._font_size,
|
||||
colour,
|
||||
self._weight,
|
||||
self._style,
|
||||
self._decoration,
|
||||
self._background
|
||||
)
|
||||
|
||||
def with_weight(self, weight: FontWeight):
|
||||
"""Create a new Font object with modified weight"""
|
||||
return Font(
|
||||
self._font_path,
|
||||
self._font_size,
|
||||
self._colour,
|
||||
weight,
|
||||
self._style,
|
||||
self._decoration,
|
||||
self._background
|
||||
)
|
||||
|
||||
def with_style(self, style: FontStyle):
|
||||
"""Create a new Font object with modified style"""
|
||||
return Font(
|
||||
self._font_path,
|
||||
self._font_size,
|
||||
self._colour,
|
||||
self._weight,
|
||||
style,
|
||||
self._decoration,
|
||||
self._background
|
||||
)
|
||||
|
||||
def with_decoration(self, decoration: TextDecoration):
|
||||
"""Create a new Font object with modified decoration"""
|
||||
return Font(
|
||||
self._font_path,
|
||||
self._font_size,
|
||||
self._colour,
|
||||
self._weight,
|
||||
self._style,
|
||||
decoration,
|
||||
self._background
|
||||
)
|
||||
@@ -1,137 +0,0 @@
|
||||
from pyWebLayout.base import Renderable
|
||||
from .concrete.box import Box
|
||||
from pyWebLayout.layout import Alignment
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
class TableCell(Box):
|
||||
def __init__(self, origin, size, content: Optional[Renderable] = None,
|
||||
callback=None, sheet=None, mode=None,
|
||||
halign=Alignment.CENTER, valign=Alignment.CENTER,
|
||||
padding: Tuple[int, int, int, int] = (5, 5, 5, 5)):
|
||||
"""
|
||||
Initialize a table cell.
|
||||
|
||||
Args:
|
||||
origin: Top-left corner coordinates
|
||||
size: Width and height of the cell
|
||||
content: Optional renderable content to place in the cell
|
||||
callback: Optional callback function
|
||||
sheet: Optional image sheet
|
||||
mode: Optional image mode
|
||||
halign: Horizontal alignment
|
||||
valign: Vertical alignment
|
||||
padding: Padding as (top, right, bottom, left)
|
||||
"""
|
||||
super().__init__(origin, size, callback, sheet, mode, halign, valign)
|
||||
self._content = content
|
||||
self._padding = padding # (top, right, bottom, left)
|
||||
|
||||
def set_content(self, content: Renderable):
|
||||
"""Set the content of this cell"""
|
||||
self._content = content
|
||||
|
||||
def render(self) -> Image:
|
||||
"""Render the cell with its content and border"""
|
||||
# Create the base canvas
|
||||
canvas = super().render()
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
|
||||
# Draw border (optional - can be customized)
|
||||
draw.rectangle([(0, 0), tuple(self._size - np.array([1, 1]))],
|
||||
outline=(0, 0, 0), width=1)
|
||||
|
||||
return canvas
|
||||
|
||||
|
||||
class Table(Box):
|
||||
def __init__(self, rows: int, columns: int, origin, size,
|
||||
cell_padding: Tuple[int, int, int, int] = (5, 5, 5, 5),
|
||||
callback=None, sheet=None, mode=None,
|
||||
halign=Alignment.CENTER, valign=Alignment.CENTER):
|
||||
"""
|
||||
Initialize a table with specified number of rows and columns.
|
||||
|
||||
Args:
|
||||
rows: Number of rows in the table
|
||||
columns: Number of columns in the table
|
||||
origin: Top-left corner coordinates
|
||||
size: Width and height of the table
|
||||
cell_padding: Padding for each cell as (top, right, bottom, left)
|
||||
callback: Optional callback function
|
||||
sheet: Optional image sheet
|
||||
mode: Optional image mode
|
||||
halign: Horizontal alignment
|
||||
valign: Vertical alignment
|
||||
"""
|
||||
super().__init__(origin, size, callback, sheet, mode, halign, valign)
|
||||
|
||||
self._rows = rows
|
||||
self._columns = columns
|
||||
self._cell_padding = cell_padding
|
||||
|
||||
# Calculate cell dimensions
|
||||
cell_width = size[0] // columns
|
||||
cell_height = size[1] // rows
|
||||
|
||||
# Create a 2D array of cells
|
||||
self._cells: List[List[TableCell]] = []
|
||||
|
||||
for row in range(rows):
|
||||
cell_row = []
|
||||
for col in range(columns):
|
||||
# Calculate cell position
|
||||
cell_origin = np.array([col * cell_width, row * cell_height])
|
||||
cell_size = np.array([cell_width, cell_height])
|
||||
|
||||
# Create the cell
|
||||
cell = TableCell(
|
||||
origin=cell_origin,
|
||||
size=cell_size,
|
||||
sheet=sheet,
|
||||
mode=mode,
|
||||
halign=halign,
|
||||
valign=valign,
|
||||
padding=cell_padding
|
||||
)
|
||||
|
||||
cell_row.append(cell)
|
||||
|
||||
self._cells.append(cell_row)
|
||||
|
||||
def add_to_cell(self, x: int, y: int, content: Renderable):
|
||||
"""
|
||||
Add content to a specific cell in the table.
|
||||
|
||||
Args:
|
||||
x: Column index (0-based)
|
||||
y: Row index (0-based)
|
||||
content: Renderable content to add to the cell
|
||||
"""
|
||||
if 0 <= y < self._rows and 0 <= x < self._columns:
|
||||
self._cells[y][x].set_content(content)
|
||||
else:
|
||||
raise IndexError(f"Cell indices ({x}, {y}) out of range. Table is {self._columns}x{self._rows}")
|
||||
|
||||
def render(self) -> Image:
|
||||
"""Render the complete table with all cells"""
|
||||
# Create base canvas
|
||||
canvas = super().render()
|
||||
|
||||
# Render each cell and paste it onto the canvas
|
||||
for row in range(self._rows):
|
||||
for col in range(self._columns):
|
||||
cell = self._cells[row][col]
|
||||
cell_img = cell.render()
|
||||
|
||||
# Get the position for this cell
|
||||
cell_pos = (col * (self._size[0] // self._columns),
|
||||
row * (self._size[1] // self._rows))
|
||||
|
||||
# Paste the cell onto the canvas
|
||||
canvas.paste(cell_img, cell_pos, cell_img)
|
||||
|
||||
return canvas
|
||||
Reference in New Issue
Block a user