table-renderer 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ from .models import Cell, Column, Row, Style, StyledObject, Table
2
+
3
+ __all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table"]
@@ -0,0 +1,8 @@
1
+ from .base import StyledObject
2
+ from .cell import Cell
3
+ from .column import Column
4
+ from .row import Row
5
+ from .style import Style
6
+ from .table import Table
7
+
8
+ __all__ = ["Cell", "Column", "Row", "Style", "StyledObject", "Table"]
@@ -0,0 +1,90 @@
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
+ size: int | None = None,
16
+ color: str | None = None,
17
+ family: str | None = None,
18
+ weight: Literal["normal", "bold"] | None = None,
19
+ ) -> Self:
20
+ """
21
+ Set font related styles.
22
+
23
+ Args:
24
+ size: Font size in pixels.
25
+ color: Text color (e.g., 'red', '#ff0000').
26
+ family: Font family or font stack.
27
+ weight: 'normal' or 'bold'.
28
+
29
+ Returns:
30
+ The object itself for chaining.
31
+ """
32
+ self.style.font_size = size or self.style.font_size
33
+ self.style.font_color = color or self.style.font_color
34
+ self.style.font_family = family or self.style.font_family
35
+ self.style.font_weight = weight or self.style.font_weight
36
+ return self
37
+
38
+ def set_background(self, color: str) -> Self:
39
+ """
40
+ Set the background color.
41
+
42
+ Args:
43
+ color: Background color.
44
+
45
+ Returns:
46
+ The object itself for chaining.
47
+ """
48
+ self.style.bg_color = color
49
+ return self
50
+
51
+ def set_align(
52
+ self,
53
+ horizontal: Literal["left", "center", "right"] | None = None,
54
+ vertical: Literal["top", "middle", "bottom"] | None = None,
55
+ ) -> Self:
56
+ """
57
+ Set text alignment.
58
+
59
+ Args:
60
+ horizontal: Horizontal alignment.
61
+ vertical: Vertical alignment.
62
+
63
+ Returns:
64
+ The object itself for chaining.
65
+ """
66
+ self.style.text_align = horizontal or self.style.text_align
67
+ self.style.vertical_align = vertical or self.style.vertical_align
68
+ return self
69
+
70
+ def set_border(
71
+ self,
72
+ width: int | None = None,
73
+ color: str | None = None,
74
+ style: Literal["solid", "dashed", "dotted"] | None = None,
75
+ ) -> Self:
76
+ """
77
+ Set border styles.
78
+
79
+ Args:
80
+ width: Border width in pixels.
81
+ color: Border color.
82
+ style: Border style.
83
+
84
+ Returns:
85
+ The object itself for chaining.
86
+ """
87
+ self.style.border_width = width or self.style.border_width
88
+ self.style.border_color = color or self.style.border_color
89
+ self.style.border_style = style or self.style.border_style
90
+ return self
@@ -0,0 +1,72 @@
1
+ from typing import Self
2
+
3
+ from .base import StyledObject
4
+
5
+
6
+ class Cell(StyledObject):
7
+ """Represents a single cell in a table."""
8
+
9
+ def __init__(self, value: str = "") -> None:
10
+ """
11
+ Initialize cell with a value.
12
+
13
+ Args:
14
+ value: The text content of the cell.
15
+ """
16
+ super().__init__()
17
+ self.value = value
18
+ self.row_span = 1
19
+ self.col_span = 1
20
+ self.is_merged = False # If True, this cell is covered by another merged cell
21
+ self.final_style: str = ""
22
+ self.image_path: str | None = None
23
+ self.image_url: str = ""
24
+ self.image_width: int | None = None
25
+ self.image_height: int | None = None
26
+
27
+ def set_text(self, text: str) -> Self:
28
+ """
29
+ Update the text content of the cell.
30
+
31
+ Args:
32
+ text: New text content.
33
+
34
+ Returns:
35
+ The cell itself for chaining.
36
+ """
37
+ self.value = text
38
+ return self
39
+
40
+ def set_image(
41
+ self, path: str, width: int | None = None, height: int | None = None
42
+ ) -> Self:
43
+ """
44
+ Set an image to be displayed in the cell.
45
+
46
+ Args:
47
+ path: Path to the image file.
48
+ width: Optional display width in pixels.
49
+ height: Optional display height in pixels.
50
+
51
+ Returns:
52
+ The cell itself for chaining.
53
+ """
54
+ self.image_path = path
55
+ self.image_width = width
56
+ self.image_height = height
57
+ return self
58
+
59
+ def span(self, rows: int = 1, cols: int = 1) -> Self:
60
+ """
61
+ Set the row and column span for merging.
62
+
63
+ Args:
64
+ rows: Number of rows to span.
65
+ cols: Number of columns to span.
66
+
67
+ Returns:
68
+ The cell itself for chaining.
69
+ """
70
+ self.row_span = rows
71
+ self.col_span = cols
72
+ return self
@@ -0,0 +1,31 @@
1
+ from typing import Self
2
+
3
+ from .base import StyledObject
4
+
5
+
6
+ class Column(StyledObject):
7
+ """Represents a column in a table."""
8
+
9
+ def __init__(self, index: int) -> None:
10
+ """
11
+ Initialize a column.
12
+
13
+ Args:
14
+ index: The 0-based index of the column.
15
+ """
16
+ super().__init__()
17
+ self.index = index
18
+ self.width: int | str = "auto"
19
+
20
+ def set_width(self, width: int | str) -> Self:
21
+ """
22
+ Set the width of the column.
23
+
24
+ Args:
25
+ width: Width in pixels (int) or CSS value (str, e.g., '10%').
26
+
27
+ Returns:
28
+ The column itself for chaining.
29
+ """
30
+ self.width = width
31
+ return self
@@ -0,0 +1,15 @@
1
+ from .base import StyledObject
2
+
3
+
4
+ class Row(StyledObject):
5
+ """Represents a row in a table."""
6
+
7
+ def __init__(self, index: int) -> None:
8
+ """
9
+ Initialize a row.
10
+
11
+ Args:
12
+ index: The 0-based index of the row.
13
+ """
14
+ super().__init__()
15
+ self.index = index
@@ -0,0 +1,107 @@
1
+ from dataclasses import dataclass
2
+ from typing import Literal
3
+
4
+
5
+ @dataclass
6
+ class Style:
7
+ """Represents the visual style of a table element."""
8
+
9
+ font_size: int | None = None
10
+ font_color: str | None = None
11
+ font_family: str | None = None
12
+ font_weight: Literal["normal", "bold"] | None = None
13
+ bg_color: str | None = None
14
+ text_align: Literal["left", "center", "right"] | None = None
15
+ vertical_align: Literal["top", "middle", "bottom"] | None = None
16
+ border_width: int | None = None
17
+ border_color: str | None = None
18
+ border_style: Literal["solid", "dashed", "dotted"] | None = None
19
+
20
+ # Detailed borders
21
+ border_top_width: int | None = None
22
+ border_top_color: str | None = None
23
+ border_top_style: Literal["solid", "dashed", "dotted"] | None = None
24
+
25
+ border_bottom_width: int | None = None
26
+ border_bottom_color: str | None = None
27
+ border_bottom_style: Literal["solid", "dashed", "dotted"] | None = None
28
+
29
+ border_left_width: int | None = None
30
+ border_left_color: str | None = None
31
+ border_left_style: Literal["solid", "dashed", "dotted"] | None = None
32
+
33
+ border_right_width: int | None = None
34
+ border_right_color: str | None = None
35
+ border_right_style: Literal["solid", "dashed", "dotted"] | None = None
36
+
37
+ def merge(self, other: "Style") -> "Style":
38
+ """
39
+ Merge another style into this one.
40
+
41
+ Args:
42
+ other: The style to merge. Attributes in 'other' take precedence.
43
+
44
+ Returns:
45
+ A new Style instance containing the merged attributes.
46
+ """
47
+ merged_style = Style(**self.__dict__)
48
+ for k, v in other.__dict__.items():
49
+ if v is not None:
50
+ setattr(merged_style, k, v)
51
+ return merged_style
52
+
53
+ def to_css(self) -> str:
54
+ """
55
+ Convert style attributes to a CSS string.
56
+
57
+ Returns:
58
+ A string containing CSS property-value pairs.
59
+ """
60
+ css = []
61
+ # Basic styles
62
+ if self.font_size:
63
+ css.append(f"font-size: {self.font_size}px;")
64
+ if self.font_color:
65
+ css.append(f"color: {self.font_color};")
66
+ if self.font_family:
67
+ # Handle font stacks: "Arial, Noto Sans" -> "'Arial', 'Noto Sans'"
68
+ families = [
69
+ f"'{f.strip()}'" if "'" not in f and '"' not in f else f.strip()
70
+ for f in self.font_family.split(",")
71
+ ]
72
+ css.append(f"font-family: {', '.join(families)};")
73
+ if self.font_weight:
74
+ css.append(f"font-weight: {self.font_weight};")
75
+ if self.bg_color:
76
+ css.append(f"background-color: {self.bg_color};")
77
+ if self.text_align:
78
+ css.append(f"text-align: {self.text_align};")
79
+ if self.vertical_align:
80
+ css.append(f"vertical-align: {self.vertical_align};")
81
+
82
+ # Borders
83
+ css.extend(self._get_border_css())
84
+ return " ".join(css)
85
+
86
+ def _get_border_css(self) -> list[str]:
87
+ """Internal helper to generate border CSS."""
88
+ css = []
89
+ if self.border_width:
90
+ css.append(f"border-width: {self.border_width}px;")
91
+ if self.border_color:
92
+ css.append(f"border-color: {self.border_color};")
93
+ if self.border_style:
94
+ css.append(f"border-style: {self.border_style};")
95
+
96
+ # Individual borders
97
+ for side in ["top", "bottom", "left", "right"]:
98
+ width = getattr(self, f"border_{side}_width")
99
+ color = getattr(self, f"border_{side}_color")
100
+ style = getattr(self, f"border_{side}_style")
101
+ if width:
102
+ css.append(f"border-{side}-width: {width}px;")
103
+ if color:
104
+ css.append(f"border-{side}-color: {color};")
105
+ if style:
106
+ css.append(f"border-{side}-style: {style};")
107
+ return css
@@ -0,0 +1,162 @@
1
+ from typing import Self
2
+
3
+ from table_renderer.renderer import render_to_image, save_html
4
+
5
+ from .base import StyledObject
6
+ from .cell import Cell
7
+ from .column import Column
8
+ from .row import Row
9
+
10
+
11
+ class Table(StyledObject):
12
+ """The main class representing a table to be rendered."""
13
+
14
+ def __init__(self, rows: int = 0, cols: int = 0) -> None:
15
+ """
16
+ Initialize a table with initial dimensions.
17
+
18
+ Args:
19
+ rows: Initial number of rows.
20
+ cols: Initial number of columns.
21
+ """
22
+ super().__init__()
23
+ self._rows_count = rows
24
+ self._cols_count = cols
25
+ self._cells = [[Cell() for _ in range(cols)] for _ in range(rows)]
26
+ self._row_objects = [Row(i) for i in range(rows)]
27
+ self._col_objects = [Column(i) for i in range(cols)]
28
+ self.width: int | str = "100%"
29
+ self.font_files: list[str] = []
30
+
31
+ def cell(self, row_index: int, col_index: int) -> Cell:
32
+ """
33
+ Get a cell at specific coordinates. Grows the table if needed.
34
+
35
+ Args:
36
+ row_index: Row index.
37
+ col_index: Column index.
38
+
39
+ Returns:
40
+ The Cell object.
41
+ """
42
+ while row_index >= len(self._cells):
43
+ self._add_row()
44
+ while col_index >= len(self._cells[0]):
45
+ self._add_col()
46
+ return self._cells[row_index][col_index]
47
+
48
+ def get_row(self, row_index: int) -> Row:
49
+ """
50
+ Get a Row object. Grows the table if needed.
51
+
52
+ Args:
53
+ row_index: Row index.
54
+
55
+ Returns:
56
+ The Row object.
57
+ """
58
+ while row_index >= len(self._row_objects):
59
+ self._add_row()
60
+ return self._row_objects[row_index]
61
+
62
+ def get_column(self, col_index: int) -> Column:
63
+ """
64
+ Get a Column object. Grows the table if needed.
65
+
66
+ Args:
67
+ col_index: Column index.
68
+
69
+ Returns:
70
+ The Column object.
71
+ """
72
+ while col_index >= len(self._col_objects):
73
+ self._add_col()
74
+ return self._col_objects[col_index]
75
+
76
+ def _add_row(self) -> None:
77
+ """Internal helper to add a row."""
78
+ new_row_idx = len(self._cells)
79
+ cols_count = len(self._cells[0]) if self._cells else self._cols_count
80
+ self._cells.append([Cell() for _ in range(cols_count)])
81
+ self._row_objects.append(Row(new_row_idx))
82
+
83
+ def _add_col(self) -> None:
84
+ """Internal helper to add a column."""
85
+ for row in self._cells:
86
+ row.append(Cell())
87
+ self._col_objects.append(Column(len(self._col_objects)))
88
+
89
+ def set_width(self, width: int | str) -> Self:
90
+ """
91
+ Set the total width of the table.
92
+
93
+ Args:
94
+ width: Width in pixels or CSS value.
95
+
96
+ Returns:
97
+ The table itself for chaining.
98
+ """
99
+ self.width = width
100
+ return self
101
+
102
+ def add_font(self, font_path: str) -> Self:
103
+ """
104
+ Register a local font file.
105
+
106
+ Args:
107
+ font_path: Path to the .ttf or .otf file.
108
+
109
+ Returns:
110
+ The table itself for chaining.
111
+ """
112
+ self.font_files.append(font_path)
113
+ return self
114
+
115
+ def to_image(self, output_path: str, dpi: int = 144, padding: int = 10) -> None:
116
+ """
117
+ Render the table to an image file.
118
+
119
+ Args:
120
+ output_path: Path to the output file (e.g., 'table.png', 'table.webp').
121
+ dpi: Resolution of the output image.
122
+ padding: Padding around the table in pixels.
123
+ """
124
+ render_to_image(self, output_path, dpi=dpi, padding=padding)
125
+
126
+ def to_html(self, output_path: str) -> None:
127
+ """
128
+ Export the table to an HTML file.
129
+
130
+ Args:
131
+ output_path: Path to the output HTML file.
132
+ """
133
+ save_html(self, output_path)
134
+
135
+ def _prepare_render(self) -> None:
136
+ """Handle merging logic and cascading styles before rendering."""
137
+ # Reset is_merged flags
138
+ for row_index in range(len(self._cells)):
139
+ for col_index in range(len(self._cells[0])):
140
+ self._cells[row_index][col_index].is_merged = False
141
+
142
+ # Mark merged cells
143
+ for row_index in range(len(self._cells)):
144
+ for col_index in range(len(self._cells[0])):
145
+ self._mark_merged_area(row_index, col_index)
146
+
147
+ def _mark_merged_area(self, row_index: int, col_index: int) -> None:
148
+ """Mark cells covered by a span as merged."""
149
+ cell = self._cells[row_index][col_index]
150
+ if cell.is_merged:
151
+ return
152
+ if cell.row_span > 1 or cell.col_span > 1:
153
+ for d_row in range(cell.row_span):
154
+ for d_col in range(cell.col_span):
155
+ if d_row == 0 and d_col == 0:
156
+ continue
157
+ if row_index + d_row < len(self._cells) and col_index + d_col < len(
158
+ self._cells[0]
159
+ ):
160
+ self._cells[row_index + d_row][
161
+ col_index + d_col
162
+ ].is_merged = True
File without changes
@@ -0,0 +1,219 @@
1
+ import os
2
+ from typing import TYPE_CHECKING
3
+
4
+ from jinja2 import Template
5
+ from weasyprint import HTML
6
+
7
+ if TYPE_CHECKING:
8
+ from .models.table import Table
9
+
10
+ HTML_TEMPLATE = """
11
+ <!DOCTYPE html>
12
+ <html>
13
+ <head>
14
+ <style>
15
+ @page {
16
+ margin: 0;
17
+ }
18
+ body {
19
+ margin: 0;
20
+ padding: 20px;
21
+ background-color: transparent;
22
+ }
23
+ {% for font in font_faces %}
24
+ @font-face {
25
+ font-family: '{{ font.name }}';
26
+ src: url('{{ font.path }}');
27
+ }
28
+ {% endfor %}
29
+
30
+ table {
31
+ border-collapse: collapse;
32
+ width: {{ table_width }};
33
+ {{ table_style }}
34
+ }
35
+
36
+ td {
37
+ padding: 8px;
38
+ box-sizing: border-box;
39
+ }
40
+
41
+ {% for col in columns %}
42
+ .col-{{ col.index }} {
43
+ width: {{ col.width }};
44
+ {{ col.style }}
45
+ }
46
+ {% endfor %}
47
+
48
+ {% for row in rows %}
49
+ .row-{{ row.index }} {
50
+ {{ row.style }}
51
+ }
52
+ {% endfor %}
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <table>
57
+ {% for r_idx in range(cells|length) %}
58
+ <tr class="row-{{ r_idx }}">
59
+ {% for c_idx in range(cells[r_idx]|length) %}
60
+ {% set cell = cells[r_idx][c_idx] %}
61
+ {% if not cell.is_merged %}
62
+ <td class="col-{{ c_idx }}"
63
+ rowspan="{{ cell.row_span }}"
64
+ colspan="{{ cell.col_span }}"
65
+ style="{{ cell.final_style }}">
66
+ {% if cell.image_url %}
67
+ <img src="{{ cell.image_url }}"
68
+ style="{% if cell.image_width %}width: {{ cell.image_width }}px;{% endif %}
69
+ {% if cell.image_height %}height: {{ cell.image_height }}px;{% endif %}">
70
+ <br>
71
+ {% endif %}
72
+ {{ cell.value | replace('\\n', '<br>') }}
73
+ </td>
74
+ {% endif %}
75
+ {% endfor %}
76
+ </tr>
77
+ {% endfor %}
78
+ </table>
79
+ </body>
80
+ </html>
81
+ """
82
+
83
+
84
+ def render_to_html(table: "Table") -> str:
85
+ """
86
+ Generate the HTML representation of the table.
87
+
88
+ Args:
89
+ table: The Table object to render.
90
+
91
+ Returns:
92
+ A string containing the complete HTML document.
93
+ """
94
+ table._prepare_render()
95
+
96
+ # Calculate final styles for each cell based on inheritance
97
+ font_faces = []
98
+ for font_path in table.font_files:
99
+ name = os.path.splitext(os.path.basename(font_path))[0]
100
+ # Convert path to absolute file URL for WeasyPrint
101
+ abs_path = os.path.abspath(font_path)
102
+ font_faces.append({"name": name, "path": f"file://{abs_path}"})
103
+
104
+ cells_data = []
105
+ for row_index in range(len(table._cells)):
106
+ row_data = []
107
+ for col_index in range(len(table._cells[0])):
108
+ 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
+
116
+ # Handle Image URL resolution
117
+ cell.image_url = ""
118
+ if cell.image_path:
119
+ abs_img_path = os.path.abspath(cell.image_path)
120
+ cell.image_url = f"file://{abs_img_path}"
121
+
122
+ # Attach final style string for template
123
+ cell.final_style = final_style.to_css()
124
+ row_data.append(cell)
125
+ cells_data.append(row_data)
126
+
127
+ template = Template(HTML_TEMPLATE)
128
+ return template.render(
129
+ table_width=f"{table.width}px" if isinstance(table.width, int) else table.width,
130
+ table_style=table.style.to_css(),
131
+ font_faces=font_faces,
132
+ columns=[
133
+ {
134
+ "index": c.index,
135
+ "width": f"{c.width}px" if isinstance(c.width, int) else c.width,
136
+ "style": c.style.to_css(),
137
+ }
138
+ for c in table._col_objects
139
+ ],
140
+ rows=[
141
+ {"index": r.index, "style": r.style.to_css()} for r in table._row_objects
142
+ ],
143
+ cells=cells_data,
144
+ )
145
+
146
+
147
+ def render_to_image(
148
+ table: "Table",
149
+ output_path: str,
150
+ dpi: int = 144,
151
+ padding: int = 10,
152
+ ) -> None:
153
+ """
154
+ Render the table to an image file.
155
+
156
+ Args:
157
+ table: The Table object to render.
158
+ output_path: Destination path for the image.
159
+ dpi: Target resolution.
160
+ padding: Margin around the table.
161
+ """
162
+ html_content = render_to_html(table)
163
+ # WeasyPrint v53+ removed write_png, so we render to PDF then convert to PNG
164
+ pdf_bytes = HTML(string=html_content).write_pdf()
165
+
166
+ import pypdfium2 as pdfium
167
+ from PIL import Image
168
+
169
+ # Load the PDF from bytes
170
+ with pdfium.PdfDocument(pdf_bytes) as pdf:
171
+ page = pdf[0]
172
+
173
+ # Render page to a bitmap
174
+ # scale_factor: 72 is the default PDF DPI
175
+ scale_factor = dpi / 72
176
+ bitmap = page.render(scale=scale_factor)
177
+
178
+ # Convert pdfium bitmap to Pillow Image
179
+ pil_image = bitmap.to_pil()
180
+ bitmap.close()
181
+
182
+ # Visual Auto-Crop: Find the bounding box of non-white pixels
183
+ # 1. Convert to RGB to ensure we have a standard background to check
184
+ bg = Image.new("RGB", pil_image.size, (255, 255, 255))
185
+ if pil_image.mode == "RGBA":
186
+ bg.paste(pil_image, mask=pil_image.split()[3])
187
+ else:
188
+ bg.paste(pil_image)
189
+
190
+ # 2. Invert and find bbox of "ink"
191
+ import PIL.ImageOps
192
+
193
+ inverted = PIL.ImageOps.invert(bg)
194
+ bbox = inverted.getbbox()
195
+
196
+ if bbox:
197
+ # Add some padding
198
+ left, top, right, bottom = bbox
199
+ left = max(0, left - padding)
200
+ top = max(0, top - padding)
201
+ right = min(pil_image.width, right + padding)
202
+ bottom = min(pil_image.height, bottom + padding)
203
+ pil_image = pil_image.crop((left, top, right, bottom))
204
+
205
+ # Save to the requested format (Pillow handles most formats)
206
+ pil_image.save(output_path)
207
+
208
+
209
+ def save_html(table: "Table", output_path: str) -> None:
210
+ """
211
+ Save the table's HTML representation to a file.
212
+
213
+ Args:
214
+ table: The Table object to export.
215
+ output_path: Path to the destination HTML file.
216
+ """
217
+ html_content = render_to_html(table)
218
+ with open(output_path, "w", encoding="utf-8") as f:
219
+ f.write(html_content)
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.4
2
+ Name: table-renderer
3
+ Version: 0.1.0
4
+ Summary: A Python library for rendering structured tables as high-quality images or HTML.
5
+ Keywords: table,renderer,rendering,image,html,weasyprint,pdfium,report
6
+ Author: pTaunium
7
+ Author-email: pTaunium <pTaunium@gmail.com>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Multimedia :: Graphics
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Dist: jinja2~=3.0
18
+ Requires-Dist: pillow~=12.0
19
+ Requires-Dist: pypdfium2~=5.0
20
+ Requires-Dist: weasyprint>=68.1
21
+ Requires-Python: >=3.12, <3.15
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Table Renderer ๐Ÿ“Š
25
+
26
+ <!-- README-I18N:START -->
27
+
28
+ **English** | [็น้ซ”ไธญๆ–‡](README.zh-TW.md)
29
+
30
+ <!-- README-I18N:END -->
31
+
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
33
+ [![Python Version](https://img.shields.io/badge/python-3.12%20%7C%203.13%20%7C%203.14-blue)](pyproject.toml)
34
+ [![CI](https://github.com/pTaunium/table-renderer/actions/workflows/ci.yml/badge.svg)](https://github.com/pTaunium/table-renderer/actions)
35
+
36
+ `table-renderer` is a powerful and lightweight Python library designed to convert structured tables into high-quality images (PNG, JPG, WebP) or HTML. By combining the layout power of CSS with an intuitive Python API, it's perfect for automated reporting, Kubernetes CronJobs, or any data visualization task.
37
+
38
+ ## โœจ Features
39
+
40
+ - **Broad Compatibility**: Fully supports **Python 3.12, 3.13, and 3.14**.
41
+ - **Business Friendly**: Uses `pypdfium2` (Apache-2.0) and `WeasyPrint` for a 100% MIT-compatible dependency chain.
42
+ - **High-Quality Rendering**: Powered by WeasyPrint (CSS engine), supporting complex text wrapping, alignment, and cell merging.
43
+ - **Auto-Crop**: Automatically crops images to the actual table area using visual detection, eliminating unnecessary white space.
44
+ - **Object-Oriented API**: Clean `Table` -> `Row/Column` -> `Cell` hierarchy with fluent interface support.
45
+ - **Cascading Styles**: Manage visual styles effortlessly with "Global Defaults + Local Overrides."
46
+ - **Multi-language Font Support**: Support for font stacks (e.g., different fonts for English and Chinese).
47
+ - **Customizable Resolution**: Adjustable DPI for high-resolution output (e.g., 300 DPI for print).
48
+ - **Lightweight Deployment**: No heavy browser dependencies (like Chromium), making it ideal for Docker/K8s.
49
+
50
+ ---
51
+
52
+ ## ๐Ÿ–ผ Visual Example
53
+
54
+ Here is an example table generated using the code in the [Quick Start](#-quick-start) section:
55
+
56
+ ![Example Table](assets/example_table.png)
57
+
58
+ ---
59
+
60
+ ## ๐Ÿš€ Quick Start
61
+
62
+ ### Installation
63
+
64
+ If you are developing within this project, use `uv`:
65
+
66
+ ```bash
67
+ uv sync
68
+ ```
69
+
70
+ To install it in another project:
71
+
72
+ ```bash
73
+ pip install .
74
+ ```
75
+
76
+ ### Basic Usage
77
+
78
+ ```python
79
+ from table_renderer import Table
80
+
81
+ # 1. Initialize a 3x3 table
82
+ table = Table(3, 3)
83
+ table.set_width(600)
84
+ table.set_border(1, "black", "solid")
85
+
86
+ # 2. Set header style (Row 0)
87
+ header = table.get_row(0)
88
+ header.set_background("#333").set_font(color="white", weight="bold", size=16)
89
+ header.set_align(horizontal="center")
90
+
91
+ table.cell(0, 0).set_text("Product")
92
+ table.cell(0, 1).set_text("Category")
93
+ table.cell(0, 2).set_text("Price")
94
+
95
+ # 3. Add content and demonstrate merging
96
+ table.cell(1, 0).set_text("MacBook Pro").span(rows=1, cols=2)
97
+ table.cell(1, 2).set_text("$2,000")
98
+
99
+ table.cell(2, 0).set_text("Magic Mouse")
100
+ table.cell(2, 1).set_text("Peripherals")
101
+ table.cell(2, 2).set_text("$79")
102
+
103
+ # 4. Global column styling (align prices to the right)
104
+ table.get_column(2).set_align(horizontal="right").set_width(100)
105
+
106
+ # 5. Export to image with custom DPI and padding
107
+ table.to_image("report.png", dpi=300, padding=20)
108
+ ```
109
+
110
+ ---
111
+
112
+ ## ๐Ÿ›  Advanced Features
113
+
114
+ ### 1. Cell Merging
115
+
116
+ Use the anchor-based design to merge cells by specifying the starting cell and the span.
117
+
118
+ ```python
119
+ # Merge first row, first two columns (1 row, 2 columns)
120
+ table.cell(0, 0).span(rows=1, cols=2).set_text("Merged Header")
121
+ ```
122
+
123
+ ### 2. Font Management
124
+
125
+ Load local font files and define font stacks for character-level fallback.
126
+
127
+ ```python
128
+ # Load local font files (.ttf / .otf)
129
+ table.add_font("./fonts/Roboto-Regular.ttf")
130
+ table.add_font("./fonts/NotoSansTC-Medium.otf")
131
+
132
+ # Define a font stack: English prefers Roboto, Chinese falls back to Noto Sans
133
+ table.set_font(family="Roboto-Regular, NotoSansTC-Medium")
134
+ ```
135
+
136
+ ### 3. Embedding Images in Cells
137
+
138
+ You can easily embed local images into any cell.
139
+
140
+ ```python
141
+ # Set an image with optional width/height constraints
142
+ table.cell(1, 0).set_image("path/to/logo.png", width=50)
143
+ table.cell(1, 0).set_text("Company Logo") # Text appears below the image
144
+ ```
145
+
146
+ ### 4. Layout & Sizing
147
+
148
+ Mix fixed widths, percentages, and auto-sizing.
149
+
150
+ ```python
151
+ table.set_width(800) # Total table width
152
+ table.get_column(0).set_width(200) # Fixed 200px
153
+ table.get_column(1).set_width("auto") # Distribute remaining space
154
+ ```
155
+
156
+ ### 5. DPI, Formats & Padding
157
+
158
+ Fine-tune your output image quality and margins.
159
+
160
+ ```python
161
+ # High-res JPG with custom 20px padding
162
+ table.to_image("output.jpg", dpi=300, padding=20)
163
+
164
+ # Lightweight WebP with tight margins
165
+ table.to_image("output.webp", dpi=72, padding=0)
166
+ ```
167
+
168
+ ---
169
+
170
+ ## ๐Ÿณ Docker / Kubernetes Deployment
171
+
172
+ Since `weasyprint` relies on system-level C libraries, your Dockerfile should include these dependencies:
173
+
174
+ ```dockerfile
175
+ FROM python:3.12-slim
176
+
177
+ # Install system dependencies for WeasyPrint rendering and fonts
178
+ RUN apt-get update && apt-get install -y --no-install-recommends \
179
+ libpango-1.0-0 \
180
+ libharfbuzz0b \
181
+ libpangoft2-1.0-0 \
182
+ libpangocairo-1.0-0 \
183
+ fonts-noto-cjk \
184
+ && rm -rf /var/lib/apt/lists/*
185
+
186
+ WORKDIR /app
187
+ COPY . .
188
+ RUN pip install .
189
+
190
+ CMD ["python", "main.py"]
191
+ ```
192
+
193
+ ---
194
+
195
+ ## ๐Ÿงช Development
196
+
197
+ ### Running Tests
198
+
199
+ We use `pytest` for testing. Ensure you have installed the development dependencies.
200
+
201
+ ```bash
202
+ uv run pytest --cov=src/table_renderer tests/
203
+ ```
204
+
205
+ ### Linting & Formatting
206
+
207
+ We use `ruff` to maintain code quality.
208
+
209
+ ```bash
210
+ uv run ruff check .
211
+ uv run ruff format .
212
+ ```
213
+
214
+ ---
215
+
216
+ ## ๐Ÿค Contributing
217
+
218
+ Contributions are welcome! If you find a bug or have a feature request, please open an issue or submit a pull request.
219
+
220
+ 1. Fork the Project
221
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
222
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
223
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
224
+ 5. Open a Pull Request
225
+
226
+ ---
227
+
228
+ ## ๐Ÿ“œ License
229
+
230
+ Distributed under the MIT License. See [`LICENSE`](LICENSE) for more information.
@@ -0,0 +1,13 @@
1
+ table_renderer/__init__.py,sha256=nggPXyPQ_F71xz_2bcutnjsJhsap0pGnwEsKJmdGktM,137
2
+ table_renderer/models/__init__.py,sha256=Ed5kC8CZoW13B340VqzMkIMUd-0Ha1fdaI8SzYy2wRw,223
3
+ table_renderer/models/base.py,sha256=A88b6A4vsK100ufthop8HA68iQPyAJ3kf4dEhTlqo84,2529
4
+ table_renderer/models/cell.py,sha256=swifMkNHbAh7AqTMWxu5RYUmVsetM9HxxS-uWamqMlU,1872
5
+ table_renderer/models/column.py,sha256=r155c3-kH3gC_0bqREu_oFCfRAChILpfyYV97Eu5WXY,697
6
+ table_renderer/models/row.py,sha256=7XJZ9T7xbLxfmt25RlRMYe7l-qWg_BkkKpe6IT09Ym4,310
7
+ table_renderer/models/style.py,sha256=UUMNS-kUoFeZ-tiULhErR4alesLsCb3MUgbpdU7iSXw,3861
8
+ table_renderer/models/table.py,sha256=a1O6DOV9s1YGzfscYYmhWMMNEC-WUOQtNuc-R4pAeRw,5100
9
+ table_renderer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ table_renderer/renderer.py,sha256=oOg-HinF68JWJ_5ESNcIb80FsRlGZ5fXaaH6IF4d1hg,6161
11
+ table_renderer-0.1.0.dist-info/WHEEL,sha256=wXwAVsgVaOZ_pwDFqQm5Rd6PID-Fc74nkLc8X8gHiDo,81
12
+ table_renderer-0.1.0.dist-info/METADATA,sha256=5iO8Kaf3oRhHicSKBZ6W2zgcxtEZME8QZcJe2OsKMnI,6774
13
+ table_renderer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.19
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any