table-renderer 0.1.1__tar.gz → 0.3.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.
- {table_renderer-0.1.1 → table_renderer-0.3.0}/PKG-INFO +16 -4
- {table_renderer-0.1.1 → table_renderer-0.3.0}/README.md +15 -3
- {table_renderer-0.1.1 → table_renderer-0.3.0}/pyproject.toml +1 -1
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/__init__.py +1 -1
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/base.py +3 -0
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/cell.py +2 -2
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/table.py +22 -5
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/renderer.py +80 -35
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/__init__.py +0 -0
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/column.py +0 -0
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/row.py +0 -0
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/models/style.py +0 -0
- {table_renderer-0.1.1 → table_renderer-0.3.0}/src/table_renderer/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table-renderer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -42,6 +42,7 @@ Description-Content-Type: text/markdown
|
|
|
42
42
|
- **Business Friendly**: Uses `pypdfium2` (Apache-2.0) and `WeasyPrint` for a 100% MIT-compatible dependency chain.
|
|
43
43
|
- **High-Quality Rendering**: Powered by WeasyPrint (CSS engine), supporting complex text wrapping, alignment, and cell merging.
|
|
44
44
|
- **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
|
|
45
|
+
- **Continuous Long Tables**: Automatically stitches multiple PDF pages into a single continuous long image, preventing content from being cut off.
|
|
45
46
|
- **Object-Oriented API**: Clean `Table` -> `Row/Column` -> `Cell` hierarchy with fluent interface support.
|
|
46
47
|
- **Cascading Styles**: Manage visual styles effortlessly with "Global Defaults + Local Overrides."
|
|
47
48
|
- **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
|
|
@@ -82,7 +83,7 @@ from table_renderer import Table
|
|
|
82
83
|
# 1. Initialize a 3x3 table
|
|
83
84
|
table = Table(3, 3)
|
|
84
85
|
table.set_width(600)
|
|
85
|
-
table.set_border(1, "black", "solid")
|
|
86
|
+
table.set_border(width=1, color="black", style="solid")
|
|
86
87
|
|
|
87
88
|
# 2. Set header style (Row 0)
|
|
88
89
|
header = table.get_row(0)
|
|
@@ -156,6 +157,15 @@ table.get_column(0).set_width(200) # Fixed 200px
|
|
|
156
157
|
table.get_column(1).set_width("auto") # Distribute remaining space
|
|
157
158
|
```
|
|
158
159
|
|
|
160
|
+
**Handling Extremely Wide Tables:**
|
|
161
|
+
The library dynamically estimates canvas width to prevent clipping. However, if your table contains unbreakable strings (like long URLs) and relies purely on auto-layout, you can manually override the canvas width. The auto-crop feature will automatically trim any excess white space.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
# Force a massive canvas width; auto-crop will cleanly trim the excess
|
|
165
|
+
table.set_width(15000)
|
|
166
|
+
table.to_image("wide_table.png")
|
|
167
|
+
```
|
|
168
|
+
|
|
159
169
|
### 5. DPI, Formats & Padding
|
|
160
170
|
|
|
161
171
|
Fine-tune your output image quality and margins.
|
|
@@ -164,6 +174,9 @@ Fine-tune your output image quality and margins.
|
|
|
164
174
|
# High-res JPG with custom 20px padding
|
|
165
175
|
table.to_image("output.jpg", dpi=300, padding=20)
|
|
166
176
|
|
|
177
|
+
# PNG with a solid white background (instead of the default transparent)
|
|
178
|
+
table.to_image("output_white.png", background_color="white")
|
|
179
|
+
|
|
167
180
|
# Lightweight WebP with tight margins
|
|
168
181
|
table.to_image("output.webp", dpi=72, padding=0)
|
|
169
182
|
```
|
|
@@ -180,9 +193,8 @@ FROM python:3.12-slim
|
|
|
180
193
|
# Install system dependencies for WeasyPrint rendering and fonts
|
|
181
194
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
182
195
|
libpango-1.0-0 \
|
|
183
|
-
libharfbuzz0b \
|
|
184
196
|
libpangoft2-1.0-0 \
|
|
185
|
-
|
|
197
|
+
libharfbuzz-subset0 \
|
|
186
198
|
fonts-noto-cjk \
|
|
187
199
|
&& rm -rf /var/lib/apt/lists/*
|
|
188
200
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- **Business Friendly**: Uses `pypdfium2` (Apache-2.0) and `WeasyPrint` for a 100% MIT-compatible dependency chain.
|
|
20
20
|
- **High-Quality Rendering**: Powered by WeasyPrint (CSS engine), supporting complex text wrapping, alignment, and cell merging.
|
|
21
21
|
- **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
|
|
22
|
+
- **Continuous Long Tables**: Automatically stitches multiple PDF pages into a single continuous long image, preventing content from being cut off.
|
|
22
23
|
- **Object-Oriented API**: Clean `Table` -> `Row/Column` -> `Cell` hierarchy with fluent interface support.
|
|
23
24
|
- **Cascading Styles**: Manage visual styles effortlessly with "Global Defaults + Local Overrides."
|
|
24
25
|
- **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
|
|
@@ -59,7 +60,7 @@ from table_renderer import Table
|
|
|
59
60
|
# 1. Initialize a 3x3 table
|
|
60
61
|
table = Table(3, 3)
|
|
61
62
|
table.set_width(600)
|
|
62
|
-
table.set_border(1, "black", "solid")
|
|
63
|
+
table.set_border(width=1, color="black", style="solid")
|
|
63
64
|
|
|
64
65
|
# 2. Set header style (Row 0)
|
|
65
66
|
header = table.get_row(0)
|
|
@@ -133,6 +134,15 @@ table.get_column(0).set_width(200) # Fixed 200px
|
|
|
133
134
|
table.get_column(1).set_width("auto") # Distribute remaining space
|
|
134
135
|
```
|
|
135
136
|
|
|
137
|
+
**Handling Extremely Wide Tables:**
|
|
138
|
+
The library dynamically estimates canvas width to prevent clipping. However, if your table contains unbreakable strings (like long URLs) and relies purely on auto-layout, you can manually override the canvas width. The auto-crop feature will automatically trim any excess white space.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Force a massive canvas width; auto-crop will cleanly trim the excess
|
|
142
|
+
table.set_width(15000)
|
|
143
|
+
table.to_image("wide_table.png")
|
|
144
|
+
```
|
|
145
|
+
|
|
136
146
|
### 5. DPI, Formats & Padding
|
|
137
147
|
|
|
138
148
|
Fine-tune your output image quality and margins.
|
|
@@ -141,6 +151,9 @@ Fine-tune your output image quality and margins.
|
|
|
141
151
|
# High-res JPG with custom 20px padding
|
|
142
152
|
table.to_image("output.jpg", dpi=300, padding=20)
|
|
143
153
|
|
|
154
|
+
# PNG with a solid white background (instead of the default transparent)
|
|
155
|
+
table.to_image("output_white.png", background_color="white")
|
|
156
|
+
|
|
144
157
|
# Lightweight WebP with tight margins
|
|
145
158
|
table.to_image("output.webp", dpi=72, padding=0)
|
|
146
159
|
```
|
|
@@ -157,9 +170,8 @@ FROM python:3.12-slim
|
|
|
157
170
|
# Install system dependencies for WeasyPrint rendering and fonts
|
|
158
171
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
159
172
|
libpango-1.0-0 \
|
|
160
|
-
libharfbuzz0b \
|
|
161
173
|
libpangoft2-1.0-0 \
|
|
162
|
-
|
|
174
|
+
libharfbuzz-subset0 \
|
|
163
175
|
fonts-noto-cjk \
|
|
164
176
|
&& rm -rf /var/lib/apt/lists/*
|
|
165
177
|
|
|
@@ -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.
|
|
9
|
+
__version__ = "0.3.0"
|
|
10
10
|
|
|
11
11
|
__all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table", "__version__"]
|
|
@@ -12,6 +12,7 @@ class StyledObject:
|
|
|
12
12
|
|
|
13
13
|
def set_font(
|
|
14
14
|
self,
|
|
15
|
+
*,
|
|
15
16
|
size: int | None = None,
|
|
16
17
|
color: str | None = None,
|
|
17
18
|
family: str | None = None,
|
|
@@ -50,6 +51,7 @@ class StyledObject:
|
|
|
50
51
|
|
|
51
52
|
def set_align(
|
|
52
53
|
self,
|
|
54
|
+
*,
|
|
53
55
|
horizontal: Literal["left", "center", "right"] | None = None,
|
|
54
56
|
vertical: Literal["top", "middle", "bottom"] | None = None,
|
|
55
57
|
) -> Self:
|
|
@@ -69,6 +71,7 @@ class StyledObject:
|
|
|
69
71
|
|
|
70
72
|
def set_border(
|
|
71
73
|
self,
|
|
74
|
+
*,
|
|
72
75
|
width: int | None = None,
|
|
73
76
|
color: str | None = None,
|
|
74
77
|
style: Literal["solid", "dashed", "dotted"] | None = None,
|
|
@@ -38,7 +38,7 @@ class Cell(StyledObject):
|
|
|
38
38
|
return self
|
|
39
39
|
|
|
40
40
|
def set_image(
|
|
41
|
-
self, path: str, width: int | None = None, height: int | None = None
|
|
41
|
+
self, path: str, *, width: int | None = None, height: int | None = None
|
|
42
42
|
) -> Self:
|
|
43
43
|
"""
|
|
44
44
|
Set an image to be displayed in the cell.
|
|
@@ -56,7 +56,7 @@ class Cell(StyledObject):
|
|
|
56
56
|
self.image_height = height
|
|
57
57
|
return self
|
|
58
58
|
|
|
59
|
-
def span(self, rows: int = 1, cols: int = 1) -> Self:
|
|
59
|
+
def span(self, *, rows: int = 1, cols: int = 1) -> Self:
|
|
60
60
|
"""
|
|
61
61
|
Set the row and column span for merging.
|
|
62
62
|
|
|
@@ -25,7 +25,7 @@ class Table(StyledObject):
|
|
|
25
25
|
self._cells = [[Cell() for _ in range(cols)] for _ in range(rows)]
|
|
26
26
|
self._row_objects = [Row(i) for i in range(rows)]
|
|
27
27
|
self._col_objects = [Column(i) for i in range(cols)]
|
|
28
|
-
self.width: int | str = "
|
|
28
|
+
self.width: int | str = "auto"
|
|
29
29
|
self.font_files: list[str] = []
|
|
30
30
|
|
|
31
31
|
def cell(self, row_index: int, col_index: int) -> Cell:
|
|
@@ -112,7 +112,14 @@ class Table(StyledObject):
|
|
|
112
112
|
self.font_files.append(font_path)
|
|
113
113
|
return self
|
|
114
114
|
|
|
115
|
-
def to_image(
|
|
115
|
+
def to_image(
|
|
116
|
+
self,
|
|
117
|
+
output_path: str,
|
|
118
|
+
*,
|
|
119
|
+
dpi: int = 144,
|
|
120
|
+
padding: int = 10,
|
|
121
|
+
background_color: str = "transparent",
|
|
122
|
+
) -> None:
|
|
116
123
|
"""
|
|
117
124
|
Render the table to an image file.
|
|
118
125
|
|
|
@@ -120,17 +127,27 @@ class Table(StyledObject):
|
|
|
120
127
|
output_path: Path to the output file (e.g., 'table.png', 'table.webp').
|
|
121
128
|
dpi: Resolution of the output image.
|
|
122
129
|
padding: Padding around the table in pixels.
|
|
130
|
+
background_color: Background color of the image.
|
|
123
131
|
"""
|
|
124
|
-
render_to_image(
|
|
132
|
+
render_to_image(
|
|
133
|
+
self,
|
|
134
|
+
output_path,
|
|
135
|
+
dpi=dpi,
|
|
136
|
+
padding=padding,
|
|
137
|
+
background_color=background_color,
|
|
138
|
+
)
|
|
125
139
|
|
|
126
|
-
def to_html(
|
|
140
|
+
def to_html(
|
|
141
|
+
self, output_path: str, *, background_color: str = "transparent"
|
|
142
|
+
) -> None:
|
|
127
143
|
"""
|
|
128
144
|
Export the table to an HTML file.
|
|
129
145
|
|
|
130
146
|
Args:
|
|
131
147
|
output_path: Path to the output HTML file.
|
|
148
|
+
background_color: Background color of the HTML body.
|
|
132
149
|
"""
|
|
133
|
-
save_html(self, output_path)
|
|
150
|
+
save_html(self, output_path, background_color=background_color)
|
|
134
151
|
|
|
135
152
|
def _prepare_render(self) -> None:
|
|
136
153
|
"""Handle merging logic and cascading styles before rendering."""
|
|
@@ -13,12 +13,13 @@ HTML_TEMPLATE = """
|
|
|
13
13
|
<head>
|
|
14
14
|
<style>
|
|
15
15
|
@page {
|
|
16
|
+
size: {{ page_width }}px {{ page_height }}px; /* Dynamically estimated size */
|
|
16
17
|
margin: 0;
|
|
17
18
|
}
|
|
18
19
|
body {
|
|
19
20
|
margin: 0;
|
|
20
21
|
padding: 20px;
|
|
21
|
-
background-color:
|
|
22
|
+
background-color: {{ background_color }};
|
|
22
23
|
}
|
|
23
24
|
{% for font in font_faces %}
|
|
24
25
|
@font-face {
|
|
@@ -36,7 +37,14 @@ table {
|
|
|
36
37
|
td {
|
|
37
38
|
padding: 8px;
|
|
38
39
|
box-sizing: border-box;
|
|
40
|
+
{{ table_style }}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
{% for row in rows %}
|
|
44
|
+
.row-{{ row.index }} {
|
|
45
|
+
{{ row.style }}
|
|
39
46
|
}
|
|
47
|
+
{% endfor %}
|
|
40
48
|
|
|
41
49
|
{% for col in columns %}
|
|
42
50
|
.col-{{ col.index }} {
|
|
@@ -44,12 +52,6 @@ td {
|
|
|
44
52
|
{{ col.style }}
|
|
45
53
|
}
|
|
46
54
|
{% endfor %}
|
|
47
|
-
|
|
48
|
-
{% for row in rows %}
|
|
49
|
-
.row-{{ row.index }} {
|
|
50
|
-
{{ row.style }}
|
|
51
|
-
}
|
|
52
|
-
{% endfor %}
|
|
53
55
|
</style>
|
|
54
56
|
</head>
|
|
55
57
|
<body>
|
|
@@ -59,10 +61,10 @@ td {
|
|
|
59
61
|
{% for c_idx in range(cells[r_idx]|length) %}
|
|
60
62
|
{% set cell = cells[r_idx][c_idx] %}
|
|
61
63
|
{% if not cell.is_merged %}
|
|
62
|
-
<td class="col-{{ c_idx }}"
|
|
64
|
+
<td class="row-{{ r_idx }} col-{{ c_idx }}"
|
|
63
65
|
rowspan="{{ cell.row_span }}"
|
|
64
66
|
colspan="{{ cell.col_span }}"
|
|
65
|
-
style="{{ cell.
|
|
67
|
+
style="{{ cell.style.to_css() }}">
|
|
66
68
|
{% if cell.image_url %}
|
|
67
69
|
<img src="{{ cell.image_url }}"
|
|
68
70
|
style="{% if cell.image_width %}width: {{ cell.image_width }}px;{% endif %}
|
|
@@ -81,12 +83,13 @@ td {
|
|
|
81
83
|
"""
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def render_to_html(table: "Table") -> str:
|
|
86
|
+
def render_to_html(table: "Table", *, background_color: str = "transparent") -> str:
|
|
85
87
|
"""
|
|
86
88
|
Generate the HTML representation of the table.
|
|
87
89
|
|
|
88
90
|
Args:
|
|
89
91
|
table: The Table object to render.
|
|
92
|
+
background_color: The background color of the body.
|
|
90
93
|
|
|
91
94
|
Returns:
|
|
92
95
|
A string containing the complete HTML document.
|
|
@@ -106,12 +109,6 @@ def render_to_html(table: "Table") -> str:
|
|
|
106
109
|
row_data = []
|
|
107
110
|
for col_index in range(len(table._cells[0])):
|
|
108
111
|
cell = table._cells[row_index][col_index]
|
|
109
|
-
row = table.get_row(row_index)
|
|
110
|
-
col = table.get_column(col_index)
|
|
111
|
-
|
|
112
|
-
# Cascading: Table -> Row/Col -> Cell
|
|
113
|
-
base_style = table.style.merge(col.style).merge(row.style)
|
|
114
|
-
final_style = base_style.merge(cell.style)
|
|
115
112
|
|
|
116
113
|
# Handle Image URL resolution
|
|
117
114
|
cell.image_url = ""
|
|
@@ -122,13 +119,33 @@ def render_to_html(table: "Table") -> str:
|
|
|
122
119
|
abs_img_path = os.path.abspath(cell.image_path)
|
|
123
120
|
cell.image_url = f"file://{abs_img_path}"
|
|
124
121
|
|
|
125
|
-
# Attach final style string for template
|
|
126
|
-
cell.final_style = final_style.to_css()
|
|
127
122
|
row_data.append(cell)
|
|
128
123
|
cells_data.append(row_data)
|
|
129
124
|
|
|
125
|
+
# Estimate safe canvas width to prevent clipping
|
|
126
|
+
# Base it on the number of columns if explicit widths aren't provided (assume ~100px min per column)
|
|
127
|
+
col_count = len(table._cells[0]) if table._cells else 0
|
|
128
|
+
estimated_width = max(2000, col_count * 100)
|
|
129
|
+
|
|
130
|
+
if isinstance(table.width, int):
|
|
131
|
+
estimated_width = max(estimated_width, table.width + 100)
|
|
132
|
+
|
|
133
|
+
col_width_sum = 0
|
|
134
|
+
for c in table._col_objects:
|
|
135
|
+
if isinstance(c.width, int):
|
|
136
|
+
col_width_sum += c.width
|
|
137
|
+
estimated_width = max(estimated_width, col_width_sum + 100)
|
|
138
|
+
|
|
139
|
+
# Estimate safe canvas height to optimize rendering speed for small tables, capped at 5000px
|
|
140
|
+
# Assume a generous average of 40px per row plus 100px padding
|
|
141
|
+
estimated_height = len(table._cells) * 40 + 100
|
|
142
|
+
estimated_height = min(max(estimated_height, 500), 5000)
|
|
143
|
+
|
|
130
144
|
template = Template(HTML_TEMPLATE)
|
|
131
145
|
return template.render(
|
|
146
|
+
page_width=estimated_width,
|
|
147
|
+
page_height=estimated_height,
|
|
148
|
+
background_color=background_color,
|
|
132
149
|
table_width=f"{table.width}px" if isinstance(table.width, int) else table.width,
|
|
133
150
|
table_style=table.style.to_css(),
|
|
134
151
|
font_faces=font_faces,
|
|
@@ -150,8 +167,10 @@ def render_to_html(table: "Table") -> str:
|
|
|
150
167
|
def render_to_image(
|
|
151
168
|
table: "Table",
|
|
152
169
|
output_path: str,
|
|
170
|
+
*,
|
|
153
171
|
dpi: int = 144,
|
|
154
172
|
padding: int = 10,
|
|
173
|
+
background_color: str = "transparent",
|
|
155
174
|
) -> None:
|
|
156
175
|
"""
|
|
157
176
|
Render the table to an image file.
|
|
@@ -161,8 +180,9 @@ def render_to_image(
|
|
|
161
180
|
output_path: Destination path for the image.
|
|
162
181
|
dpi: Target resolution.
|
|
163
182
|
padding: Margin around the table.
|
|
183
|
+
background_color: Background color of the image.
|
|
164
184
|
"""
|
|
165
|
-
html_content = render_to_html(table)
|
|
185
|
+
html_content = render_to_html(table, background_color=background_color)
|
|
166
186
|
# WeasyPrint v53+ removed write_png, so we render to PDF then convert to PNG
|
|
167
187
|
pdf_bytes = HTML(string=html_content).write_pdf()
|
|
168
188
|
|
|
@@ -171,29 +191,45 @@ def render_to_image(
|
|
|
171
191
|
|
|
172
192
|
# Load the PDF from bytes
|
|
173
193
|
with pdfium.PdfDocument(pdf_bytes) as pdf:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
# Render page to a bitmap
|
|
177
|
-
# scale_factor: 72 is the default PDF DPI
|
|
194
|
+
page_images = []
|
|
178
195
|
scale_factor = dpi / 72
|
|
179
|
-
bitmap = page.render(scale=scale_factor)
|
|
180
196
|
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
# Render each page to a PIL Image
|
|
198
|
+
for page in pdf:
|
|
199
|
+
# fill_color=(255, 255, 255, 0) ensures the background is transparent
|
|
200
|
+
bitmap = page.render(scale=scale_factor, fill_color=(255, 255, 255, 0))
|
|
201
|
+
pil_page = bitmap.to_pil()
|
|
202
|
+
page_images.append(pil_page)
|
|
203
|
+
bitmap.close()
|
|
204
|
+
|
|
205
|
+
if not page_images:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Stitch all pages vertically into one long image
|
|
209
|
+
total_width = max(img.width for img in page_images)
|
|
210
|
+
total_height = sum(img.height for img in page_images)
|
|
211
|
+
|
|
212
|
+
# Create a large canvas
|
|
213
|
+
pil_image = Image.new("RGBA", (total_width, total_height), (255, 255, 255, 0))
|
|
214
|
+
y_offset = 0
|
|
215
|
+
for img in page_images:
|
|
216
|
+
pil_image.paste(img, (0, y_offset))
|
|
217
|
+
y_offset += img.height
|
|
184
218
|
|
|
185
219
|
# Visual Auto-Crop: Find the bounding box of non-white pixels
|
|
186
|
-
#
|
|
187
|
-
bg = Image.new("RGB", pil_image.size, (255, 255, 255))
|
|
220
|
+
# Use 'L' (grayscale) mode to reduce memory usage (1 byte per pixel vs 3)
|
|
188
221
|
if pil_image.mode == "RGBA":
|
|
189
|
-
|
|
222
|
+
# Flatten RGBA onto a white grayscale background
|
|
223
|
+
grayscale = Image.new("L", pil_image.size, 255)
|
|
224
|
+
grayscale.paste(pil_image.convert("L"), mask=pil_image.getchannel("A"))
|
|
190
225
|
else:
|
|
191
|
-
|
|
226
|
+
# For RGB or other modes, just convert to grayscale
|
|
227
|
+
grayscale = pil_image.convert("L")
|
|
192
228
|
|
|
193
|
-
#
|
|
229
|
+
# Invert and find bbox of "ink" (non-white pixels)
|
|
194
230
|
import PIL.ImageOps
|
|
195
231
|
|
|
196
|
-
inverted = PIL.ImageOps.invert(
|
|
232
|
+
inverted = PIL.ImageOps.invert(grayscale)
|
|
197
233
|
bbox = inverted.getbbox()
|
|
198
234
|
|
|
199
235
|
if bbox:
|
|
@@ -206,17 +242,26 @@ def render_to_image(
|
|
|
206
242
|
pil_image = pil_image.crop((left, top, right, bottom))
|
|
207
243
|
|
|
208
244
|
# Save to the requested format (Pillow handles most formats)
|
|
245
|
+
if output_path.lower().endswith((".jpg", ".jpeg")) and pil_image.mode == "RGBA":
|
|
246
|
+
# Create a white background image and paste the RGBA image on it to flatten it
|
|
247
|
+
rgb_image = Image.new("RGB", pil_image.size, (255, 255, 255))
|
|
248
|
+
rgb_image.paste(pil_image, mask=pil_image.split()[3])
|
|
249
|
+
pil_image = rgb_image
|
|
250
|
+
|
|
209
251
|
pil_image.save(output_path)
|
|
210
252
|
|
|
211
253
|
|
|
212
|
-
def save_html(
|
|
254
|
+
def save_html(
|
|
255
|
+
table: "Table", output_path: str, *, background_color: str = "transparent"
|
|
256
|
+
) -> None:
|
|
213
257
|
"""
|
|
214
258
|
Save the table's HTML representation to a file.
|
|
215
259
|
|
|
216
260
|
Args:
|
|
217
261
|
table: The Table object to export.
|
|
218
262
|
output_path: Path to the destination HTML file.
|
|
263
|
+
background_color: Background color of the HTML body.
|
|
219
264
|
"""
|
|
220
|
-
html_content = render_to_html(table)
|
|
265
|
+
html_content = render_to_html(table, background_color=background_color)
|
|
221
266
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
222
267
|
f.write(html_content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|