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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: table-renderer
3
- Version: 0.1.1
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
- libpangocairo-1.0-0 \
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
- libpangocairo-1.0-0 \
174
+ libharfbuzz-subset0 \
163
175
  fonts-noto-cjk \
164
176
  && rm -rf /var/lib/apt/lists/*
165
177
 
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "table-renderer"
7
- version = "0.1.1"
7
+ version = "0.3.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"
@@ -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.1.1"
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 = "100%"
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(self, output_path: str, dpi: int = 144, padding: int = 10) -> None:
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(self, output_path, dpi=dpi, padding=padding)
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(self, output_path: str) -> None:
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: transparent;
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.final_style }}">
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
- page = pdf[0]
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
- # Convert pdfium bitmap to Pillow Image
182
- pil_image = bitmap.to_pil()
183
- bitmap.close()
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
- # 1. Convert to RGB to ensure we have a standard background to check
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
- bg.paste(pil_image, mask=pil_image.split()[3])
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
- bg.paste(pil_image)
226
+ # For RGB or other modes, just convert to grayscale
227
+ grayscale = pil_image.convert("L")
192
228
 
193
- # 2. Invert and find bbox of "ink"
229
+ # Invert and find bbox of "ink" (non-white pixels)
194
230
  import PIL.ImageOps
195
231
 
196
- inverted = PIL.ImageOps.invert(bg)
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(table: "Table", output_path: str) -> None:
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)