table-renderer 0.3.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table-renderer
3
- Version: 0.3.0
3
+ Version: 0.5.0
4
4
  Summary: A Python library for rendering structured tables as high-quality images or HTML.
5
5
  Keywords: table,renderer,rendering,image,html,weasyprint,pdfium,report
6
6
  Author: pTaunium
@@ -17,6 +17,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
17
  Requires-Dist: jinja2~=3.0
18
18
  Requires-Dist: pillow~=12.0
19
19
  Requires-Dist: pypdfium2~=5.0
20
+ Requires-Dist: typing-extensions>=4.5.0
20
21
  Requires-Dist: weasyprint>=68.1
21
22
  Requires-Python: >=3.12, <3.15
22
23
  Description-Content-Type: text/markdown
@@ -44,7 +45,7 @@ Description-Content-Type: text/markdown
44
45
  - **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
45
46
  - **Continuous Long Tables**: Automatically stitches multiple PDF pages into a single continuous long image, preventing content from being cut off.
46
47
  - **Object-Oriented API**: Clean `Table` -> `Row/Column` -> `Cell` hierarchy with fluent interface support.
47
- - **Cascading Styles**: Manage visual styles effortlessly with "Global Defaults + Local Overrides."
48
+ - **Cascading Styles**: Manage visual styles effortlessly with logical precedence: `Cell` > `Row` > `Column` > `Table`.
48
49
  - **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
49
50
  - **Customizable Resolution**: Adjustable DPI for high-resolution output (e.g., 300 DPI for print).
50
51
  - **Lightweight Deployment**: No heavy browser dependencies (like Chromium), making it ideal for Docker/K8s.
@@ -82,8 +83,8 @@ from table_renderer import Table
82
83
 
83
84
  # 1. Initialize a 3x3 table
84
85
  table = Table(3, 3)
85
- table.set_width(600)
86
86
  table.set_border(width=1, color="black", style="solid")
87
+ table.set_width(400)
87
88
 
88
89
  # 2. Set header style (Row 0)
89
90
  header = table.get_row(0)
@@ -95,7 +96,7 @@ table.cell(0, 1).set_text("Category")
95
96
  table.cell(0, 2).set_text("Price")
96
97
 
97
98
  # 3. Add content and demonstrate merging
98
- table.cell(1, 0).set_text("MacBook Pro").span(rows=1, cols=2)
99
+ table.cell(1, 0).set_text("MacBook Pro").set_span(rows=1, cols=2)
99
100
  table.cell(1, 2).set_text("$2,000")
100
101
 
101
102
  table.cell(2, 0).set_text("Magic Mouse")
@@ -105,8 +106,8 @@ table.cell(2, 2).set_text("$79")
105
106
  # 4. Global column styling (align prices to the right)
106
107
  table.get_column(2).set_align(horizontal="right").set_width(100)
107
108
 
108
- # 5. Export to image with custom DPI and padding
109
- table.to_image("report.png", dpi=300, padding=20)
109
+ # 5. Export to image with custom DPI, padding, and background color
110
+ table.to_image("report.png", dpi=300, padding=20, background_color="white")
110
111
  ```
111
112
 
112
113
  ---
@@ -119,7 +120,7 @@ Use the anchor-based design to merge cells by specifying the starting cell and t
119
120
 
120
121
  ```python
121
122
  # Merge first row, first two columns (1 row, 2 columns)
122
- table.cell(0, 0).span(rows=1, cols=2).set_text("Merged Header")
123
+ table.cell(0, 0).set_span(rows=1, cols=2).set_text("Merged Header")
123
124
  ```
124
125
 
125
126
  ### 2. Font Management
@@ -149,12 +150,13 @@ table.cell(1, 1).set_image("https://example.com/image.jpg", width=100)
149
150
 
150
151
  ### 4. Layout & Sizing
151
152
 
152
- Mix fixed widths, percentages, and auto-sizing.
153
+ Mix fixed widths, heights, percentages, and auto-sizing.
153
154
 
154
155
  ```python
155
156
  table.set_width(800) # Total table width
156
- table.get_column(0).set_width(200) # Fixed 200px
157
+ table.get_column(0).set_width(200) # Fixed column width (200px)
157
158
  table.get_column(1).set_width("auto") # Distribute remaining space
159
+ table.get_row(0).set_height(100) # Fixed row height (100px)
158
160
  ```
159
161
 
160
162
  **Handling Extremely Wide Tables:**
@@ -21,7 +21,7 @@
21
21
  - **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
22
22
  - **Continuous Long Tables**: Automatically stitches multiple PDF pages into a single continuous long image, preventing content from being cut off.
23
23
  - **Object-Oriented API**: Clean `Table` -> `Row/Column` -> `Cell` hierarchy with fluent interface support.
24
- - **Cascading Styles**: Manage visual styles effortlessly with "Global Defaults + Local Overrides."
24
+ - **Cascading Styles**: Manage visual styles effortlessly with logical precedence: `Cell` > `Row` > `Column` > `Table`.
25
25
  - **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
26
26
  - **Customizable Resolution**: Adjustable DPI for high-resolution output (e.g., 300 DPI for print).
27
27
  - **Lightweight Deployment**: No heavy browser dependencies (like Chromium), making it ideal for Docker/K8s.
@@ -59,8 +59,8 @@ from table_renderer import Table
59
59
 
60
60
  # 1. Initialize a 3x3 table
61
61
  table = Table(3, 3)
62
- table.set_width(600)
63
62
  table.set_border(width=1, color="black", style="solid")
63
+ table.set_width(400)
64
64
 
65
65
  # 2. Set header style (Row 0)
66
66
  header = table.get_row(0)
@@ -72,7 +72,7 @@ table.cell(0, 1).set_text("Category")
72
72
  table.cell(0, 2).set_text("Price")
73
73
 
74
74
  # 3. Add content and demonstrate merging
75
- table.cell(1, 0).set_text("MacBook Pro").span(rows=1, cols=2)
75
+ table.cell(1, 0).set_text("MacBook Pro").set_span(rows=1, cols=2)
76
76
  table.cell(1, 2).set_text("$2,000")
77
77
 
78
78
  table.cell(2, 0).set_text("Magic Mouse")
@@ -82,8 +82,8 @@ table.cell(2, 2).set_text("$79")
82
82
  # 4. Global column styling (align prices to the right)
83
83
  table.get_column(2).set_align(horizontal="right").set_width(100)
84
84
 
85
- # 5. Export to image with custom DPI and padding
86
- table.to_image("report.png", dpi=300, padding=20)
85
+ # 5. Export to image with custom DPI, padding, and background color
86
+ table.to_image("report.png", dpi=300, padding=20, background_color="white")
87
87
  ```
88
88
 
89
89
  ---
@@ -96,7 +96,7 @@ Use the anchor-based design to merge cells by specifying the starting cell and t
96
96
 
97
97
  ```python
98
98
  # Merge first row, first two columns (1 row, 2 columns)
99
- table.cell(0, 0).span(rows=1, cols=2).set_text("Merged Header")
99
+ table.cell(0, 0).set_span(rows=1, cols=2).set_text("Merged Header")
100
100
  ```
101
101
 
102
102
  ### 2. Font Management
@@ -126,12 +126,13 @@ table.cell(1, 1).set_image("https://example.com/image.jpg", width=100)
126
126
 
127
127
  ### 4. Layout & Sizing
128
128
 
129
- Mix fixed widths, percentages, and auto-sizing.
129
+ Mix fixed widths, heights, percentages, and auto-sizing.
130
130
 
131
131
  ```python
132
132
  table.set_width(800) # Total table width
133
- table.get_column(0).set_width(200) # Fixed 200px
133
+ table.get_column(0).set_width(200) # Fixed column width (200px)
134
134
  table.get_column(1).set_width("auto") # Distribute remaining space
135
+ table.get_row(0).set_height(100) # Fixed row height (100px)
135
136
  ```
136
137
 
137
138
  **Handling Extremely Wide Tables:**
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "table-renderer"
7
- version = "0.3.0"
7
+ version = "0.5.0"
8
8
  description = "A Python library for rendering structured tables as high-quality images or HTML."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "jinja2~=3.0",
28
28
  "pillow~=12.0",
29
29
  "pypdfium2~=5.0",
30
+ "typing-extensions>=4.5.0",
30
31
  "weasyprint>=68.1",
31
32
  ]
32
33
 
@@ -6,6 +6,6 @@ try:
6
6
  __version__ = importlib.metadata.version("table-renderer")
7
7
  except importlib.metadata.PackageNotFoundError:
8
8
  # Package is not installed (e.g. during local development without pip install -e .)
9
- __version__ = "0.3.0"
9
+ __version__ = "0.5.0"
10
10
 
11
11
  __all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table", "__version__"]
@@ -0,0 +1,6 @@
1
+ """Rendering backends for table-renderer."""
2
+
3
+ from .base import ImageRenderer
4
+ from .weasyprint_renderer import WeasyPrintRenderer
5
+
6
+ __all__ = ["ImageRenderer", "WeasyPrintRenderer"]
@@ -0,0 +1,44 @@
1
+ """Protocol defining the image rendering backend interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Protocol
6
+
7
+ if TYPE_CHECKING:
8
+ from PIL.Image import Image
9
+
10
+ from table_renderer.models.table import Table
11
+
12
+
13
+ class ImageRenderer(Protocol):
14
+ """Interface for rendering a Table model to an image.
15
+
16
+ All rendering backends must implement this interface. This allows
17
+ swapping the underlying rendering engine (e.g., WeasyPrint, Blitz)
18
+ without changing the public API or HTML generation logic.
19
+ """
20
+
21
+ def render(
22
+ self,
23
+ table: Table,
24
+ *,
25
+ dpi: int = 144,
26
+ padding: int = 10,
27
+ background_color: str = "transparent",
28
+ ) -> Image:
29
+ """Render a Table to a tightly-cropped PIL Image.
30
+
31
+ The returned image should be cropped to content bounds
32
+ (plus the specified padding). It is the backend's
33
+ responsibility to ensure no excess whitespace.
34
+
35
+ Args:
36
+ table: The Table data model to render.
37
+ dpi: Target resolution in dots per inch.
38
+ padding: Pixels of padding around content.
39
+ background_color: Background color.
40
+
41
+ Returns:
42
+ A PIL Image (RGBA mode) tightly cropped to the table.
43
+ """
44
+ ...
@@ -0,0 +1,271 @@
1
+ """WeasyPrint-based rendering backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from weasyprint import HTML
8
+
9
+ from table_renderer.html_generator import render_to_html
10
+
11
+ if TYPE_CHECKING:
12
+ from PIL.Image import Image
13
+
14
+ from table_renderer.models.cell import Cell
15
+ from table_renderer.models.table import Table
16
+
17
+ _DEFAULT_COL_WIDTH = 100
18
+ _DEFAULT_FONT_SIZE = 16
19
+ _CELL_PADDING = 8
20
+ _LINE_HEIGHT_RATIO = 1.4
21
+ _MIN_PAGE_WIDTH = 200
22
+ _MIN_PAGE_HEIGHT = 200
23
+ _PAGE_PADDING = 60 # body padding (20px * 2) + safety margin
24
+
25
+
26
+ class WeasyPrintRenderer:
27
+ """Renders tables via WeasyPrint (HTML → PDF → raster image).
28
+
29
+ This is the default rendering backend. It converts HTML to PDF
30
+ via WeasyPrint, rasterizes the PDF pages to bitmaps via pypdfium2,
31
+ and auto-crops excess whitespace from the PDF page.
32
+ """
33
+
34
+ def render(
35
+ self,
36
+ table: Table,
37
+ *,
38
+ dpi: int = 144,
39
+ padding: int = 10,
40
+ background_color: str = "transparent",
41
+ ) -> Image:
42
+ """Render a Table to a tightly-cropped PIL Image.
43
+
44
+ Args:
45
+ table: The Table data model to render.
46
+ dpi: Target resolution in dots per inch.
47
+ padding: Pixels of padding around content.
48
+ background_color: Background color.
49
+
50
+ Returns:
51
+ A PIL Image (RGBA mode) tightly cropped to the table.
52
+ """
53
+ page_css = _estimate_page_css(table)
54
+ html = render_to_html(
55
+ table, background_color=background_color, extra_css=page_css
56
+ )
57
+
58
+ # WeasyPrint v53+ removed write_png, so we render to PDF then convert
59
+ pdf_bytes = HTML(string=html).write_pdf()
60
+ full_page_image = _pdf_to_image(pdf_bytes, dpi=dpi)
61
+ return _auto_crop(full_page_image, padding=padding)
62
+
63
+
64
+ def _estimate_page_css(table: Table) -> str:
65
+ """Generate ``@page`` CSS rules with estimated canvas size.
66
+
67
+ Estimates the PDF page dimensions based on the actual table
68
+ content — column widths, font sizes, text line counts, and
69
+ embedded image heights — rather than fixed magic numbers.
70
+
71
+ Args:
72
+ table: The Table object to estimate dimensions for.
73
+
74
+ Returns:
75
+ A CSS string containing the ``@page`` rule.
76
+ """
77
+ estimated_width = _estimate_width(table)
78
+ estimated_height = _estimate_height(table)
79
+
80
+ return (
81
+ "@page {\n"
82
+ f" size: {estimated_width}px {estimated_height}px;"
83
+ " /* Dynamically estimated size */\n"
84
+ " margin: 0;\n"
85
+ "}"
86
+ )
87
+
88
+
89
+ def _estimate_width(table: Table) -> int:
90
+ """Estimate the required page width in pixels.
91
+
92
+ Uses explicit column widths where available and falls back
93
+ to a default per-column width for unset columns.
94
+
95
+ Args:
96
+ table: The Table to estimate width for.
97
+
98
+ Returns:
99
+ Estimated width in pixels.
100
+ """
101
+ if not table._cells:
102
+ return _MIN_PAGE_WIDTH
103
+
104
+ # If the table has an explicit pixel width, use it directly
105
+ if isinstance(table.width, int):
106
+ return max(_MIN_PAGE_WIDTH, table.width + _PAGE_PADDING)
107
+
108
+ # Sum column widths: use explicit value or default
109
+ total_width = 0
110
+ for col in table._col_objects:
111
+ if isinstance(col.width, int):
112
+ total_width += col.width
113
+ else:
114
+ total_width += _DEFAULT_COL_WIDTH
115
+
116
+ return max(_MIN_PAGE_WIDTH, total_width + _PAGE_PADDING)
117
+
118
+
119
+ def _estimate_height(table: Table) -> int:
120
+ """Estimate the required page height in pixels.
121
+
122
+ Calculates a per-row height based on the tallest cell in
123
+ each row, considering font size, number of text lines, and
124
+ embedded image height.
125
+
126
+ Args:
127
+ table: The Table to estimate height for.
128
+
129
+ Returns:
130
+ Estimated height in pixels.
131
+ """
132
+ if not table._cells:
133
+ return _MIN_PAGE_HEIGHT
134
+
135
+ # Resolve the table-level default font size
136
+ table_font_size = table.style.font_size or _DEFAULT_FONT_SIZE
137
+
138
+ total_height = 0
139
+ for row_idx, row_cells in enumerate(table._cells):
140
+ row_obj = table._row_objects[row_idx]
141
+
142
+ # If an explicit integer height is provided, use it
143
+ if isinstance(row_obj.height, int):
144
+ total_height += row_obj.height
145
+ continue
146
+
147
+ # Resolve row-level font size
148
+ row_font_size = row_obj.style.font_size or table_font_size
149
+
150
+ row_height = 0
151
+ for cell in row_cells:
152
+ if cell.is_merged:
153
+ continue
154
+
155
+ cell_height = _estimate_cell_height(cell, default_font_size=row_font_size)
156
+ row_height = max(row_height, cell_height)
157
+
158
+ total_height += row_height
159
+
160
+ return max(_MIN_PAGE_HEIGHT, total_height + _PAGE_PADDING)
161
+
162
+
163
+ def _estimate_cell_height(cell: Cell, *, default_font_size: int) -> int:
164
+ """Estimate the height of a single cell in pixels.
165
+
166
+ Accounts for font size, number of text lines (split by newlines),
167
+ cell padding, and embedded image height.
168
+
169
+ Args:
170
+ cell: The Cell to estimate.
171
+ default_font_size: Inherited font size if the cell has none set.
172
+
173
+ Returns:
174
+ Estimated cell height in pixels.
175
+ """
176
+ font_size = cell.style.font_size or default_font_size
177
+
178
+ # Text height: number of lines * font_size * line-height + padding
179
+ line_count = max(1, cell.value.count("\n") + 1) if cell.value else 1
180
+ text_height = int(line_count * font_size * _LINE_HEIGHT_RATIO) + _CELL_PADDING * 2
181
+
182
+ # Image height (if present)
183
+ image_height = (cell.image_height or 0) + (_CELL_PADDING if cell.image_path else 0)
184
+
185
+ return text_height + image_height
186
+
187
+
188
+ def _pdf_to_image(pdf_bytes: bytes, *, dpi: int) -> Image:
189
+ """Convert PDF bytes to a single stitched PIL Image.
190
+
191
+ Renders each page of the PDF at the specified DPI and
192
+ stitches them vertically into one tall image.
193
+
194
+ Args:
195
+ pdf_bytes: Raw PDF bytes.
196
+ dpi: Target resolution in dots per inch.
197
+
198
+ Returns:
199
+ A PIL RGBA Image with all pages stitched vertically.
200
+ """
201
+ import pypdfium2 as pdfium
202
+ from PIL import Image
203
+
204
+ with pdfium.PdfDocument(pdf_bytes) as pdf:
205
+ rendered_pages = []
206
+ scale_factor = dpi / 72 # PDF standard resolution is 72 DPI
207
+
208
+ # Render each page to a PIL Image
209
+ for page in pdf:
210
+ # fill_color=(255, 255, 255, 0) ensures transparent background
211
+ bitmap = page.render(scale=scale_factor, fill_color=(255, 255, 255, 0))
212
+ page_image = bitmap.to_pil()
213
+ rendered_pages.append(page_image)
214
+ bitmap.close()
215
+
216
+ if not rendered_pages:
217
+ return Image.new("RGBA", (1, 1), (255, 255, 255, 0))
218
+
219
+ # Stitch all pages vertically into one long image
220
+ total_width = max(img.width for img in rendered_pages)
221
+ total_height = sum(img.height for img in rendered_pages)
222
+ stitched = Image.new("RGBA", (total_width, total_height), (255, 255, 255, 0))
223
+
224
+ y_offset = 0
225
+ for img in rendered_pages:
226
+ stitched.paste(img, (0, y_offset))
227
+ y_offset += img.height
228
+
229
+ return stitched
230
+
231
+
232
+ def _auto_crop(image: Image, *, padding: int) -> Image:
233
+ """Crop excess PDF page whitespace around the table.
234
+
235
+ WeasyPrint renders to a full PDF page, so the rendered image
236
+ typically has large margins that need to be trimmed.
237
+
238
+ Args:
239
+ image: The source PIL Image.
240
+ padding: Pixels of padding to preserve around content.
241
+
242
+ Returns:
243
+ The cropped image.
244
+ """
245
+ import PIL.ImageOps
246
+ from PIL import Image
247
+
248
+ # Find the bounding box of non-transparent/non-white content.
249
+ # Use 'L' (grayscale) mode to reduce memory usage (1 byte per pixel vs 3).
250
+ if image.mode == "RGBA":
251
+ # Flatten RGBA onto a white grayscale background
252
+ grayscale = Image.new("L", image.size, 255)
253
+ grayscale.paste(image.convert("L"), mask=image.getchannel("A"))
254
+ else:
255
+ # For RGB or other modes, just convert to grayscale
256
+ grayscale = image.convert("L")
257
+
258
+ # Invert and find bbox of "ink" (non-white pixels)
259
+ inverted = PIL.ImageOps.invert(grayscale)
260
+ bbox = inverted.getbbox()
261
+
262
+ if bbox:
263
+ # Expand crop region by the requested padding, clamped to image bounds
264
+ left, top, right, bottom = bbox
265
+ left = max(0, left - padding)
266
+ top = max(0, top - padding)
267
+ right = min(image.width, right + padding)
268
+ bottom = min(image.height, bottom + padding)
269
+ image = image.crop((left, top, right, bottom))
270
+
271
+ return image
@@ -0,0 +1,126 @@
1
+ """HTML generation from Table models.
2
+
3
+ This module converts Table data models into HTML document strings.
4
+ It provides shared utilities that rendering backends can use, but
5
+ is not tied to any specific rendering engine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ from jinja2 import Environment, FileSystemLoader, Template
15
+
16
+ if TYPE_CHECKING:
17
+ from .models.table import Table
18
+
19
+ # HTML template loaded from templates/table.html.j2
20
+ _JINJA_ENV: Environment | None = None
21
+
22
+
23
+ def _get_template() -> Template:
24
+ """Return the cached Jinja2 table template, initializing the environment on first call."""
25
+ global _JINJA_ENV
26
+ if _JINJA_ENV is None:
27
+ template_dir = Path(__file__).parent / "templates"
28
+ _JINJA_ENV = Environment(loader=FileSystemLoader(template_dir)) # noqa: S701
29
+ return _JINJA_ENV.get_template("table.html.j2")
30
+
31
+
32
+ def _format_length(value: int | str) -> str:
33
+ """Format an integer as pixels, or return a string CSS value as is."""
34
+ return f"{value}px" if isinstance(value, int) else value
35
+
36
+
37
+ def _prepare_render_context(
38
+ table: Table, *, background_color: str = "transparent"
39
+ ) -> dict[str, Any]:
40
+ """Prepare the Jinja2 template context from a Table model.
41
+
42
+ Handles merge-flag setup, font-face resolution, image URL
43
+ resolution, and extraction of column/row/cell data.
44
+
45
+ Args:
46
+ table: The Table object to prepare.
47
+ background_color: The background color of the body.
48
+
49
+ Returns:
50
+ A dict suitable for passing to ``Template.render()``.
51
+ """
52
+ table._prepare_render()
53
+
54
+ font_faces = []
55
+ for font_path in table.font_files:
56
+ font_name = os.path.splitext(os.path.basename(font_path))[0]
57
+ # Convert path to absolute file URL for rendering engines
58
+ absolute_font_path = os.path.abspath(font_path)
59
+ font_faces.append({"name": font_name, "path": f"file://{absolute_font_path}"})
60
+
61
+ cell_matrix = []
62
+ for row_index in range(len(table._cells)):
63
+ cell_row = []
64
+ for col_index in range(len(table._cells[0])):
65
+ cell = table._cells[row_index][col_index]
66
+
67
+ # Handle Image URL resolution
68
+ cell.image_url = ""
69
+ if cell.image_path:
70
+ if cell.image_path.startswith(("http://", "https://")):
71
+ cell.image_url = cell.image_path
72
+ else:
73
+ absolute_image_path = os.path.abspath(cell.image_path)
74
+ cell.image_url = f"file://{absolute_image_path}"
75
+
76
+ cell_row.append(cell)
77
+ cell_matrix.append(cell_row)
78
+
79
+ return {
80
+ "background_color": background_color,
81
+ "table_width": _format_length(table.width),
82
+ "table_style": table.style.to_css(),
83
+ "font_faces": font_faces,
84
+ "columns": [
85
+ {
86
+ "index": c.index,
87
+ "width": _format_length(c.width),
88
+ "style": c.style.to_css(),
89
+ }
90
+ for c in table._col_objects
91
+ ],
92
+ "rows": [
93
+ {
94
+ "index": r.index,
95
+ "height": _format_length(r.height),
96
+ "style": r.style.to_css(),
97
+ }
98
+ for r in table._row_objects
99
+ ],
100
+ "cells": cell_matrix,
101
+ }
102
+
103
+
104
+ def render_to_html(
105
+ table: Table,
106
+ *,
107
+ background_color: str = "transparent",
108
+ extra_css: str = "",
109
+ ) -> str:
110
+ """Generate the HTML representation of the table.
111
+
112
+ Args:
113
+ table: The Table object to render.
114
+ background_color: The background color of the body.
115
+ extra_css: Additional CSS to inject into the document head.
116
+ Used by backends to add engine-specific rules
117
+ (e.g., ``@page`` rules for WeasyPrint).
118
+
119
+ Returns:
120
+ A string containing the complete HTML document.
121
+ """
122
+ context = _prepare_render_context(table, background_color=background_color)
123
+ context["extra_css"] = extra_css
124
+ template = _get_template()
125
+
126
+ return template.render(**context)
@@ -1,8 +1,7 @@
1
- from .base import StyledObject
2
1
  from .cell import Cell
3
2
  from .column import Column
4
3
  from .row import Row
5
- from .style import Style
4
+ from .style import Style, StyledObject
6
5
  from .table import Table
7
6
 
8
7
  __all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table"]
@@ -1,6 +1,8 @@
1
1
  from typing import Self
2
2
 
3
- from .base import StyledObject
3
+ from typing_extensions import deprecated
4
+
5
+ from .style import StyledObject
4
6
 
5
7
 
6
8
  class Cell(StyledObject):
@@ -56,9 +58,8 @@ class Cell(StyledObject):
56
58
  self.image_height = height
57
59
  return self
58
60
 
59
- def span(self, *, rows: int = 1, cols: int = 1) -> Self:
60
- """
61
- Set the row and column span for merging.
61
+ def set_span(self, *, rows: int = 1, cols: int = 1) -> Self:
62
+ """Set the row and column span for merging.
62
63
 
63
64
  Args:
64
65
  rows: Number of rows to span.
@@ -70,3 +71,26 @@ class Cell(StyledObject):
70
71
  self.row_span = rows
71
72
  self.col_span = cols
72
73
  return self
74
+
75
+ @deprecated("Use `set_span` instead.")
76
+ def span(self, *, rows: int = 1, cols: int = 1) -> Self:
77
+ """Set the row and column span for merging.
78
+
79
+ .. deprecated::
80
+ Use :meth:`set_span` instead.
81
+
82
+ Args:
83
+ rows: Number of rows to span.
84
+ cols: Number of columns to span.
85
+
86
+ Returns:
87
+ The cell itself for chaining.
88
+ """
89
+ import warnings
90
+
91
+ warnings.warn(
92
+ "Cell.span() is deprecated, use Cell.set_span() instead.",
93
+ DeprecationWarning,
94
+ stacklevel=2,
95
+ )
96
+ return self.set_span(rows=rows, cols=cols)
@@ -1,6 +1,6 @@
1
1
  from typing import Self
2
2
 
3
- from .base import StyledObject
3
+ from .style import StyledObject
4
4
 
5
5
 
6
6
  class Column(StyledObject):