table-renderer 0.2.0__tar.gz → 0.4.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.2.0
3
+ Version: 0.4.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
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
44
44
  - **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
45
45
  - **Continuous Long Tables**: Automatically stitches multiple PDF pages into a single continuous long image, preventing content from being cut off.
46
46
  - **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."
47
+ - **Cascading Styles**: Manage visual styles effortlessly with logical precedence: `Cell` > `Row` > `Column` > `Table`.
48
48
  - **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
49
49
  - **Customizable Resolution**: Adjustable DPI for high-resolution output (e.g., 300 DPI for print).
50
50
  - **Lightweight Deployment**: No heavy browser dependencies (like Chromium), making it ideal for Docker/K8s.
@@ -82,7 +82,6 @@ from table_renderer import Table
82
82
 
83
83
  # 1. Initialize a 3x3 table
84
84
  table = Table(3, 3)
85
- table.set_width(600)
86
85
  table.set_border(width=1, color="black", style="solid")
87
86
 
88
87
  # 2. Set header style (Row 0)
@@ -105,8 +104,8 @@ table.cell(2, 2).set_text("$79")
105
104
  # 4. Global column styling (align prices to the right)
106
105
  table.get_column(2).set_align(horizontal="right").set_width(100)
107
106
 
108
- # 5. Export to image with custom DPI and padding
109
- table.to_image("report.png", dpi=300, padding=20)
107
+ # 5. Export to image with custom DPI, padding, and background color
108
+ table.to_image("report.png", dpi=300, padding=20, background_color="white")
110
109
  ```
111
110
 
112
111
  ---
@@ -174,6 +173,9 @@ Fine-tune your output image quality and margins.
174
173
  # High-res JPG with custom 20px padding
175
174
  table.to_image("output.jpg", dpi=300, padding=20)
176
175
 
176
+ # PNG with a solid white background (instead of the default transparent)
177
+ table.to_image("output_white.png", background_color="white")
178
+
177
179
  # Lightweight WebP with tight margins
178
180
  table.to_image("output.webp", dpi=72, padding=0)
179
181
  ```
@@ -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,7 +59,6 @@ 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")
64
63
 
65
64
  # 2. Set header style (Row 0)
@@ -82,8 +81,8 @@ table.cell(2, 2).set_text("$79")
82
81
  # 4. Global column styling (align prices to the right)
83
82
  table.get_column(2).set_align(horizontal="right").set_width(100)
84
83
 
85
- # 5. Export to image with custom DPI and padding
86
- table.to_image("report.png", dpi=300, padding=20)
84
+ # 5. Export to image with custom DPI, padding, and background color
85
+ table.to_image("report.png", dpi=300, padding=20, background_color="white")
87
86
  ```
88
87
 
89
88
  ---
@@ -151,6 +150,9 @@ Fine-tune your output image quality and margins.
151
150
  # High-res JPG with custom 20px padding
152
151
  table.to_image("output.jpg", dpi=300, padding=20)
153
152
 
153
+ # PNG with a solid white background (instead of the default transparent)
154
+ table.to_image("output_white.png", background_color="white")
155
+
154
156
  # Lightweight WebP with tight margins
155
157
  table.to_image("output.webp", dpi=72, padding=0)
156
158
  ```
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "table-renderer"
7
- version = "0.2.0"
7
+ version = "0.4.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.2.0"
9
+ __version__ = "0.4.0"
10
10
 
11
11
  __all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table", "__version__"]
@@ -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,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 Cell(StyledObject):
@@ -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):
@@ -1,4 +1,4 @@
1
- from .base import StyledObject
1
+ from .style import StyledObject
2
2
 
3
3
 
4
4
  class Row(StyledObject):
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Literal
2
+ from typing import Literal, Self
3
3
 
4
4
 
5
5
  @dataclass
@@ -105,3 +105,93 @@ class Style:
105
105
  if style:
106
106
  css.append(f"border-{side}-style: {style};")
107
107
  return css
108
+
109
+
110
+ class StyledObject:
111
+ """Base class for objects that can be styled."""
112
+
113
+ def __init__(self) -> None:
114
+ """Initialize with a default Style."""
115
+ self.style = Style()
116
+
117
+ def set_font(
118
+ self,
119
+ *,
120
+ size: int | None = None,
121
+ color: str | None = None,
122
+ family: str | None = None,
123
+ weight: Literal["normal", "bold"] | None = None,
124
+ ) -> Self:
125
+ """
126
+ Set font related styles.
127
+
128
+ Args:
129
+ size: Font size in pixels.
130
+ color: Text color (e.g., 'red', '#ff0000').
131
+ family: Font family or font stack.
132
+ weight: 'normal' or 'bold'.
133
+
134
+ Returns:
135
+ The object itself for chaining.
136
+ """
137
+ self.style.font_size = size or self.style.font_size
138
+ self.style.font_color = color or self.style.font_color
139
+ self.style.font_family = family or self.style.font_family
140
+ self.style.font_weight = weight or self.style.font_weight
141
+ return self
142
+
143
+ def set_background(self, color: str) -> Self:
144
+ """
145
+ Set the background color.
146
+
147
+ Args:
148
+ color: Background color.
149
+
150
+ Returns:
151
+ The object itself for chaining.
152
+ """
153
+ self.style.bg_color = color
154
+ return self
155
+
156
+ def set_align(
157
+ self,
158
+ *,
159
+ horizontal: Literal["left", "center", "right"] | None = None,
160
+ vertical: Literal["top", "middle", "bottom"] | None = None,
161
+ ) -> Self:
162
+ """
163
+ Set text alignment.
164
+
165
+ Args:
166
+ horizontal: Horizontal alignment.
167
+ vertical: Vertical alignment.
168
+
169
+ Returns:
170
+ The object itself for chaining.
171
+ """
172
+ self.style.text_align = horizontal or self.style.text_align
173
+ self.style.vertical_align = vertical or self.style.vertical_align
174
+ return self
175
+
176
+ def set_border(
177
+ self,
178
+ *,
179
+ width: int | None = None,
180
+ color: str | None = None,
181
+ style: Literal["solid", "dashed", "dotted"] | None = None,
182
+ ) -> Self:
183
+ """
184
+ Set border styles.
185
+
186
+ Args:
187
+ width: Border width in pixels.
188
+ color: Border color.
189
+ style: Border style.
190
+
191
+ Returns:
192
+ The object itself for chaining.
193
+ """
194
+ self.style.border_width = width or self.style.border_width
195
+ self.style.border_color = color or self.style.border_color
196
+ self.style.border_style = style or self.style.border_style
197
+ return self
@@ -2,10 +2,10 @@ from typing import Self
2
2
 
3
3
  from table_renderer.renderer import render_to_image, save_html
4
4
 
5
- from .base import StyledObject
6
5
  from .cell import Cell
7
6
  from .column import Column
8
7
  from .row import Row
8
+ from .style import StyledObject
9
9
 
10
10
 
11
11
  class Table(StyledObject):
@@ -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."""
@@ -19,7 +19,7 @@ HTML_TEMPLATE = """
19
19
  body {
20
20
  margin: 0;
21
21
  padding: 20px;
22
- background-color: transparent;
22
+ background-color: {{ background_color }};
23
23
  }
24
24
  {% for font in font_faces %}
25
25
  @font-face {
@@ -37,6 +37,7 @@ table {
37
37
  td {
38
38
  padding: 8px;
39
39
  box-sizing: border-box;
40
+ {{ table_style }}
40
41
  }
41
42
 
42
43
  {% for col in columns %}
@@ -60,10 +61,10 @@ td {
60
61
  {% for c_idx in range(cells[r_idx]|length) %}
61
62
  {% set cell = cells[r_idx][c_idx] %}
62
63
  {% if not cell.is_merged %}
63
- <td class="col-{{ c_idx }}"
64
+ <td class="row-{{ r_idx }} col-{{ c_idx }}"
64
65
  rowspan="{{ cell.row_span }}"
65
66
  colspan="{{ cell.col_span }}"
66
- style="{{ cell.final_style }}">
67
+ style="{{ cell.style.to_css() }}">
67
68
  {% if cell.image_url %}
68
69
  <img src="{{ cell.image_url }}"
69
70
  style="{% if cell.image_width %}width: {{ cell.image_width }}px;{% endif %}
@@ -82,12 +83,13 @@ td {
82
83
  """
83
84
 
84
85
 
85
- def render_to_html(table: "Table") -> str:
86
+ def render_to_html(table: "Table", *, background_color: str = "transparent") -> str:
86
87
  """
87
88
  Generate the HTML representation of the table.
88
89
 
89
90
  Args:
90
91
  table: The Table object to render.
92
+ background_color: The background color of the body.
91
93
 
92
94
  Returns:
93
95
  A string containing the complete HTML document.
@@ -107,12 +109,6 @@ def render_to_html(table: "Table") -> str:
107
109
  row_data = []
108
110
  for col_index in range(len(table._cells[0])):
109
111
  cell = table._cells[row_index][col_index]
110
- row = table.get_row(row_index)
111
- col = table.get_column(col_index)
112
-
113
- # Cascading: Table -> Row/Col -> Cell
114
- base_style = table.style.merge(col.style).merge(row.style)
115
- final_style = base_style.merge(cell.style)
116
112
 
117
113
  # Handle Image URL resolution
118
114
  cell.image_url = ""
@@ -123,8 +119,6 @@ def render_to_html(table: "Table") -> str:
123
119
  abs_img_path = os.path.abspath(cell.image_path)
124
120
  cell.image_url = f"file://{abs_img_path}"
125
121
 
126
- # Attach final style string for template
127
- cell.final_style = final_style.to_css()
128
122
  row_data.append(cell)
129
123
  cells_data.append(row_data)
130
124
 
@@ -151,6 +145,7 @@ def render_to_html(table: "Table") -> str:
151
145
  return template.render(
152
146
  page_width=estimated_width,
153
147
  page_height=estimated_height,
148
+ background_color=background_color,
154
149
  table_width=f"{table.width}px" if isinstance(table.width, int) else table.width,
155
150
  table_style=table.style.to_css(),
156
151
  font_faces=font_faces,
@@ -175,6 +170,7 @@ def render_to_image(
175
170
  *,
176
171
  dpi: int = 144,
177
172
  padding: int = 10,
173
+ background_color: str = "transparent",
178
174
  ) -> None:
179
175
  """
180
176
  Render the table to an image file.
@@ -184,8 +180,9 @@ def render_to_image(
184
180
  output_path: Destination path for the image.
185
181
  dpi: Target resolution.
186
182
  padding: Margin around the table.
183
+ background_color: Background color of the image.
187
184
  """
188
- html_content = render_to_html(table)
185
+ html_content = render_to_html(table, background_color=background_color)
189
186
  # WeasyPrint v53+ removed write_png, so we render to PDF then convert to PNG
190
187
  pdf_bytes = HTML(string=html_content).write_pdf()
191
188
 
@@ -220,17 +217,19 @@ def render_to_image(
220
217
  y_offset += img.height
221
218
 
222
219
  # Visual Auto-Crop: Find the bounding box of non-white pixels
223
- # 1. Convert to RGB to ensure we have a standard background to check
224
- 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)
225
221
  if pil_image.mode == "RGBA":
226
- 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"))
227
225
  else:
228
- bg.paste(pil_image)
226
+ # For RGB or other modes, just convert to grayscale
227
+ grayscale = pil_image.convert("L")
229
228
 
230
- # 2. Invert and find bbox of "ink"
229
+ # Invert and find bbox of "ink" (non-white pixels)
231
230
  import PIL.ImageOps
232
231
 
233
- inverted = PIL.ImageOps.invert(bg)
232
+ inverted = PIL.ImageOps.invert(grayscale)
234
233
  bbox = inverted.getbbox()
235
234
 
236
235
  if bbox:
@@ -252,14 +251,17 @@ def render_to_image(
252
251
  pil_image.save(output_path)
253
252
 
254
253
 
255
- 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:
256
257
  """
257
258
  Save the table's HTML representation to a file.
258
259
 
259
260
  Args:
260
261
  table: The Table object to export.
261
262
  output_path: Path to the destination HTML file.
263
+ background_color: Background color of the HTML body.
262
264
  """
263
- html_content = render_to_html(table)
265
+ html_content = render_to_html(table, background_color=background_color)
264
266
  with open(output_path, "w", encoding="utf-8") as f:
265
267
  f.write(html_content)
@@ -1,93 +0,0 @@
1
- from typing import Literal, Self
2
-
3
- from .style import Style
4
-
5
-
6
- class StyledObject:
7
- """Base class for objects that can be styled."""
8
-
9
- def __init__(self) -> None:
10
- """Initialize with a default Style."""
11
- self.style = Style()
12
-
13
- def set_font(
14
- self,
15
- *,
16
- size: int | None = None,
17
- color: str | None = None,
18
- family: str | None = None,
19
- weight: Literal["normal", "bold"] | None = None,
20
- ) -> Self:
21
- """
22
- Set font related styles.
23
-
24
- Args:
25
- size: Font size in pixels.
26
- color: Text color (e.g., 'red', '#ff0000').
27
- family: Font family or font stack.
28
- weight: 'normal' or 'bold'.
29
-
30
- Returns:
31
- The object itself for chaining.
32
- """
33
- self.style.font_size = size or self.style.font_size
34
- self.style.font_color = color or self.style.font_color
35
- self.style.font_family = family or self.style.font_family
36
- self.style.font_weight = weight or self.style.font_weight
37
- return self
38
-
39
- def set_background(self, color: str) -> Self:
40
- """
41
- Set the background color.
42
-
43
- Args:
44
- color: Background color.
45
-
46
- Returns:
47
- The object itself for chaining.
48
- """
49
- self.style.bg_color = color
50
- return self
51
-
52
- def set_align(
53
- self,
54
- *,
55
- horizontal: Literal["left", "center", "right"] | None = None,
56
- vertical: Literal["top", "middle", "bottom"] | None = None,
57
- ) -> Self:
58
- """
59
- Set text alignment.
60
-
61
- Args:
62
- horizontal: Horizontal alignment.
63
- vertical: Vertical alignment.
64
-
65
- Returns:
66
- The object itself for chaining.
67
- """
68
- self.style.text_align = horizontal or self.style.text_align
69
- self.style.vertical_align = vertical or self.style.vertical_align
70
- return self
71
-
72
- def set_border(
73
- self,
74
- *,
75
- width: int | None = None,
76
- color: str | None = None,
77
- style: Literal["solid", "dashed", "dotted"] | None = None,
78
- ) -> Self:
79
- """
80
- Set border styles.
81
-
82
- Args:
83
- width: Border width in pixels.
84
- color: Border color.
85
- style: Border style.
86
-
87
- Returns:
88
- The object itself for chaining.
89
- """
90
- self.style.border_width = width or self.style.border_width
91
- self.style.border_color = color or self.style.border_color
92
- self.style.border_style = style or self.style.border_style
93
- return self