kiaisoft commited on
Commit
5cf94fe
·
verified ·
1 Parent(s): 7804d7a

Upload render.py

Browse files
Files changed (1) hide show
  1. render.py +436 -0
render.py ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Table Canvas Renderer
4
+ Renders all tables from JSON data onto a single white canvas with specified dimensions
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from typing import Dict, List, Tuple, Any, Optional
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ import argparse
12
+
13
+ class TableCanvasRenderer:
14
+ """Renders multiple tables from JSON data onto a canvas"""
15
+
16
+ def __init__(self):
17
+ """Initialize the table canvas renderer"""
18
+ self.DEFAULT_FONT_SIZE = 11
19
+ self.CELL_PADDING = 4
20
+ self.DEFAULT_BORDER_WIDTH = 1
21
+ self.DEFAULT_BORDER_COLOR = "#000000"
22
+ self.DEFAULT_BACKGROUND_COLOR = "#ffffff"
23
+
24
+ def get_font(self, font_family: str = None, font_size: int = None, font_weight: str = "normal"):
25
+ """Get PIL font object with fallbacks"""
26
+ if font_size is None:
27
+ font_size = self.DEFAULT_FONT_SIZE
28
+
29
+ # Try to load the specified font
30
+ font_paths = []
31
+
32
+ if font_family:
33
+ if font_family.lower() == "ms gothic":
34
+ # Common paths for MS Gothic on different systems
35
+ font_paths.extend([
36
+ "C:/Windows/Fonts/msgothic.ttc",
37
+ "/System/Library/Fonts/Hiragino Sans GB.ttc",
38
+ "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf"
39
+ ])
40
+ else:
41
+ font_paths.append(font_family)
42
+
43
+ # Fallback fonts
44
+ font_paths.extend([
45
+ "arial.ttf", "Arial.ttf",
46
+ "/System/Library/Fonts/Arial.ttf",
47
+ "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
48
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
49
+ ])
50
+
51
+ for font_path in font_paths:
52
+ try:
53
+ font = ImageFont.truetype(font_path, font_size)
54
+ # For bold text, try to find bold variant
55
+ if font_weight == "bold":
56
+ bold_path = font_path.replace("Regular", "Bold").replace(".ttf", "b.ttf")
57
+ try:
58
+ font = ImageFont.truetype(bold_path, font_size)
59
+ except:
60
+ pass # Use regular font if bold not available
61
+ return font
62
+ except (OSError, IOError):
63
+ continue
64
+
65
+ # Ultimate fallback to default PIL font
66
+ try:
67
+ return ImageFont.load_default()
68
+ except:
69
+ return None
70
+
71
+ def hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int, int]:
72
+ """Convert hex color to RGBA tuple"""
73
+ if not hex_color or hex_color == "transparent":
74
+ return (255, 255, 255, 0) # Transparent
75
+
76
+ hex_color = hex_color.lstrip('#')
77
+ if len(hex_color) == 6:
78
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,)
79
+ return (255, 255, 255, 255) # Default white
80
+
81
+ def extract_tables_from_json(self, json_data: Any) -> List[Dict]:
82
+ """Extract all table items from JSON data"""
83
+ if isinstance(json_data, list):
84
+ tables = [item for item in json_data if item.get("type") == "table"]
85
+ elif isinstance(json_data, dict) and json_data.get("type") == "table":
86
+ tables = [json_data]
87
+ else:
88
+ tables = []
89
+ return tables
90
+
91
+ def calculate_cell_coordinates(self, table_properties: Dict) -> Dict[Tuple[int, int], Dict]:
92
+ """Calculate coordinates for all visible cells in the table"""
93
+ rows = table_properties.get("rows", 0)
94
+ columns = table_properties.get("columns", 0)
95
+ column_widths = table_properties.get("columnWidths", {})
96
+ row_heights = table_properties.get("rowHeights", {})
97
+ merged_cells = table_properties.get("mergedCells", {})
98
+ hidden_cells = table_properties.get("hiddenCells", {})
99
+
100
+ def get_col_width(col: int) -> int:
101
+ return column_widths.get(str(col), 100)
102
+
103
+ def get_row_height(row: int) -> int:
104
+ return row_heights.get(str(row), 30)
105
+
106
+ # Build set of cells that are covered by merged cells (excluding origin)
107
+ merged_spanned_cells = set()
108
+ for cell_key, merge_info in merged_cells.items():
109
+ base_row, base_col = map(int, cell_key.split('-'))
110
+ rowspan = merge_info.get('rowspan', 1)
111
+ colspan = merge_info.get('colspan', 1)
112
+
113
+ # Add all spanned cells except the origin cell
114
+ for r in range(base_row, base_row + rowspan):
115
+ for c in range(base_col, base_col + colspan):
116
+ if (r, c) != (base_row, base_col):
117
+ merged_spanned_cells.add((r, c))
118
+
119
+ cell_coords = {}
120
+
121
+ for row in range(rows):
122
+ for col in range(columns):
123
+ cell_key = f"{row}-{col}"
124
+
125
+ # Skip hidden cells and cells covered by merges
126
+ if hidden_cells.get(cell_key) or (row, col) in merged_spanned_cells:
127
+ continue
128
+
129
+ # Calculate position by summing previous column widths/row heights
130
+ x = sum(get_col_width(c) for c in range(col))
131
+ y = sum(get_row_height(r) for r in range(row))
132
+
133
+ # Check if this cell is a merge origin
134
+ if cell_key in merged_cells:
135
+ merge_info = merged_cells[cell_key]
136
+ colspan = merge_info.get("colspan", 1)
137
+ rowspan = merge_info.get("rowspan", 1)
138
+ else:
139
+ colspan = 1
140
+ rowspan = 1
141
+
142
+ # Calculate cell dimensions
143
+ width = sum(get_col_width(c) for c in range(col, col + colspan))
144
+ height = sum(get_row_height(r) for r in range(row, row + rowspan))
145
+
146
+ cell_coords[(row, col)] = {
147
+ "x": x,
148
+ "y": y,
149
+ "width": width,
150
+ "height": height,
151
+ "colspan": colspan,
152
+ "rowspan": rowspan
153
+ }
154
+
155
+ return cell_coords
156
+
157
+ def determine_cell_borders(self, cell_data: Optional[Dict], table_properties: Dict) -> Tuple[int, int, int, int]:
158
+ """Determine border visibility for each side of a cell"""
159
+ # Get global border settings
160
+ cell_borders = table_properties.get("cellBorders", {})
161
+ has_global_borders = cell_borders.get("all", False)
162
+
163
+ # Default borders based on global setting
164
+ borders = {
165
+ "top": 1 if has_global_borders else 0,
166
+ "bottom": 1 if has_global_borders else 0,
167
+ "left": 1 if has_global_borders else 0,
168
+ "right": 1 if has_global_borders else 0
169
+ }
170
+
171
+ # Check for cell-specific border overrides
172
+ if cell_data and "cellStyle" in cell_data:
173
+ cell_style = cell_data["cellStyle"]
174
+
175
+ # Border property mappings
176
+ border_mappings = {
177
+ "borderTopWidth": "top",
178
+ "borderBottomWidth": "bottom",
179
+ "borderLeftWidth": "left",
180
+ "borderRightWidth": "right"
181
+ }
182
+
183
+ # If any border width property exists, this cell has custom borders
184
+ has_custom_borders = any(key in cell_style for key in border_mappings.keys())
185
+
186
+ if has_custom_borders:
187
+ # Apply custom border settings for each side
188
+ for width_key, border_side in border_mappings.items():
189
+ if width_key in cell_style:
190
+ # Check border width
191
+ width = cell_style[width_key]
192
+ has_border = width > 0
193
+
194
+ # Check border style if specified
195
+ style_key = width_key.replace("Width", "Style")
196
+ if style_key in cell_style:
197
+ style = cell_style[style_key]
198
+ if style == "none":
199
+ has_border = False
200
+
201
+ borders[border_side] = 1 if has_border else 0
202
+
203
+ return borders["top"], borders["bottom"], borders["left"], borders["right"]
204
+
205
+ def draw_table_on_canvas(self, draw: ImageDraw.Draw, table_data: Dict):
206
+ """Draw a single table on the canvas"""
207
+ properties = table_data.get("properties", {})
208
+ table_x = table_data.get("x", 0)
209
+ table_y = table_data.get("y", 0)
210
+ table_width = table_data.get("width", properties.get("width", 800))
211
+ table_height = table_data.get("height", properties.get("height", 600))
212
+
213
+ # Calculate cell coordinates
214
+ cell_coords = self.calculate_cell_coordinates(properties)
215
+
216
+ # Get table styling
217
+ font_size = properties.get("fontSize", self.DEFAULT_FONT_SIZE)
218
+ font_family = properties.get("fontFamily", "Arial")
219
+ table_bg_color = self.hex_to_rgb(properties.get("backgroundColor", self.DEFAULT_BACKGROUND_COLOR))
220
+
221
+ # Draw table background if specified
222
+ if table_bg_color != (255, 255, 255, 255): # If not default white
223
+ draw.rectangle([
224
+ (table_x, table_y),
225
+ (table_x + table_width, table_y + table_height)
226
+ ], fill=table_bg_color)
227
+
228
+ # Get cell data and styling
229
+ cell_data = properties.get("cellData", {})
230
+ header_bg_color = self.hex_to_rgb(properties.get("headerBackgroundColor", "#e8f7f0"))
231
+
232
+ # Draw cells
233
+ for (row, col), coords in cell_coords.items():
234
+ cell_key = f"{row}-{col}"
235
+ current_cell_data = cell_data.get(cell_key, {})
236
+
237
+ # Calculate cell position on canvas
238
+ cell_x = table_x + coords["x"]
239
+ cell_y = table_y + coords["y"]
240
+ cell_w = coords["width"]
241
+ cell_h = coords["height"]
242
+
243
+ # Get cell styling
244
+ cell_style = current_cell_data.get("cellStyle", {})
245
+ cell_bg_color = cell_style.get("backgroundColor")
246
+
247
+ # Use header background for header rows if no specific background
248
+ if row == 0 and not cell_bg_color:
249
+ cell_bg_color = properties.get("headerBackgroundColor", "#e8f7f0")
250
+ elif not cell_bg_color:
251
+ cell_bg_color = properties.get("backgroundColor", self.DEFAULT_BACKGROUND_COLOR)
252
+
253
+ # Draw cell background
254
+ if cell_bg_color and cell_bg_color != "transparent":
255
+ bg_color = self.hex_to_rgb(cell_bg_color)
256
+ draw.rectangle([
257
+ (cell_x, cell_y),
258
+ (cell_x + cell_w, cell_y + cell_h)
259
+ ], fill=bg_color)
260
+
261
+ # Draw cell borders
262
+ top, bottom, left, right = self.determine_cell_borders(current_cell_data, properties)
263
+ border_color = self.hex_to_rgb(
264
+ cell_style.get("borderBottomColor",
265
+ cell_style.get("borderColor", self.DEFAULT_BORDER_COLOR))
266
+ )[:3] # Remove alpha for line drawing
267
+
268
+ border_width = max(1, cell_style.get("borderWidth", self.DEFAULT_BORDER_WIDTH))
269
+
270
+ if top:
271
+ draw.line([(cell_x, cell_y), (cell_x + cell_w, cell_y)],
272
+ fill=border_color, width=border_width)
273
+ if bottom:
274
+ draw.line([(cell_x, cell_y + cell_h), (cell_x + cell_w, cell_y + cell_h)],
275
+ fill=border_color, width=border_width)
276
+ if left:
277
+ draw.line([(cell_x, cell_y), (cell_x, cell_y + cell_h)],
278
+ fill=border_color, width=border_width)
279
+ if right:
280
+ draw.line([(cell_x + cell_w, cell_y), (cell_x + cell_w, cell_y + cell_h)],
281
+ fill=border_color, width=border_width)
282
+
283
+ # Draw cell text
284
+ cell_value = current_cell_data.get("value", "")
285
+ if cell_value:
286
+ # Get text styling
287
+ text_color = self.hex_to_rgb(
288
+ cell_style.get("color",
289
+ properties.get("color", "#000000"))
290
+ )[:3] # Remove alpha
291
+
292
+ font_weight = cell_style.get("fontWeight", "normal")
293
+ if row == 0: # Header row
294
+ font_weight = properties.get("headerFontWeight", "bold")
295
+
296
+ text_align = cell_style.get("textAlign", "left")
297
+ cell_font_size = cell_style.get("fontSize", font_size)
298
+
299
+ # Get font
300
+ font = self.get_font(font_family, cell_font_size, font_weight)
301
+
302
+ # Calculate text position
303
+ try:
304
+ text_bbox = draw.textbbox((0, 0), str(cell_value), font=font)
305
+ text_width = text_bbox[2] - text_bbox[0]
306
+ text_height = text_bbox[3] - text_bbox[1]
307
+ except:
308
+ # Fallback for older PIL versions
309
+ text_width, text_height = draw.textsize(str(cell_value), font=font)
310
+
311
+ # Position based on alignment
312
+ if text_align == "center":
313
+ text_x = cell_x + (cell_w - text_width) // 2
314
+ elif text_align == "right":
315
+ text_x = cell_x + cell_w - text_width - self.CELL_PADDING
316
+ else: # left
317
+ text_x = cell_x + self.CELL_PADDING
318
+
319
+ text_y = cell_y + (cell_h - text_height) // 2
320
+
321
+ # Draw text
322
+ draw.text((text_x, text_y), str(cell_value),
323
+ fill=text_color, font=font)
324
+
325
+ def render_tables_to_canvas(self, json_data: Any, canvas_width: int, canvas_height: int) -> Image.Image:
326
+ """
327
+ Render all tables from JSON data onto a white canvas
328
+
329
+ Args:
330
+ json_data: JSON data containing table definitions
331
+ canvas_width: Width of the canvas
332
+ canvas_height: Height of the canvas
333
+
334
+ Returns:
335
+ PIL Image with all tables rendered
336
+ """
337
+ # Create white canvas
338
+ canvas = Image.new('RGBA', (canvas_width, canvas_height), (255, 255, 255, 255))
339
+ draw = ImageDraw.Draw(canvas)
340
+
341
+ # Extract all tables from JSON
342
+ tables = self.extract_tables_from_json(json_data)
343
+
344
+ if not tables:
345
+ print("No tables found in JSON data")
346
+ return canvas
347
+
348
+ print(f"Found {len(tables)} table(s) to render")
349
+
350
+ # Draw each table on the canvas
351
+ for i, table_data in enumerate(tables):
352
+ try:
353
+ table_id = table_data.get("id", f"table_{i}")
354
+ print(f" Drawing table {i+1}/{len(tables)} (id: {table_id})")
355
+ self.draw_table_on_canvas(draw, table_data)
356
+ except Exception as e:
357
+ print(f" Error drawing table {i+1}: {e}")
358
+ continue
359
+
360
+ print("Canvas rendering complete")
361
+ return canvas
362
+
363
+ def render_from_file(self, json_file: str, canvas_width: int, canvas_height: int, output_file: str = None) -> Image.Image:
364
+ """
365
+ Render tables from JSON file to canvas
366
+
367
+ Args:
368
+ json_file: Path to JSON file
369
+ canvas_width: Width of the canvas
370
+ canvas_height: Height of the canvas
371
+ output_file: Optional path to save the rendered canvas
372
+
373
+ Returns:
374
+ PIL Image with all tables rendered
375
+ """
376
+ # Read JSON data
377
+ try:
378
+ with open(json_file, 'r', encoding='utf-8') as f:
379
+ json_data = json.load(f)
380
+ except Exception as e:
381
+ print(f"Error reading JSON file: {e}")
382
+ return None
383
+
384
+ # Render to canvas
385
+ canvas = self.render_tables_to_canvas(json_data, canvas_width, canvas_height)
386
+
387
+ # Save if output file specified
388
+ if output_file:
389
+ try:
390
+ canvas.save(output_file)
391
+ print(f"Canvas saved to: {output_file}")
392
+ except Exception as e:
393
+ print(f"Error saving canvas: {e}")
394
+
395
+ return canvas
396
+
397
+
398
+ def main():
399
+ """Main function for command line usage"""
400
+ parser = argparse.ArgumentParser(description="Render tables from JSON data to canvas")
401
+ parser.add_argument("json_file", help="Path to JSON file containing table data")
402
+ parser.add_argument("width", type=int, help="Canvas width in pixels")
403
+ parser.add_argument("height", type=int, help="Canvas height in pixels")
404
+ parser.add_argument("--output", "-o", help="Output image file path (optional)")
405
+
406
+ args = parser.parse_args()
407
+
408
+ # Validate arguments
409
+ if args.width <= 0 or args.height <= 0:
410
+ print("Canvas width and height must be positive integers")
411
+ return
412
+
413
+ if not os.path.exists(args.json_file):
414
+ print(f"JSON file not found: {args.json_file}")
415
+ return
416
+
417
+ # Generate output filename if not provided
418
+ output_file = args.output
419
+ if not output_file:
420
+ base_name = os.path.splitext(os.path.basename(args.json_file))[0]
421
+ output_file = f"{base_name}_canvas_{args.width}x{args.height}.png"
422
+
423
+ # Create renderer and process file
424
+ renderer = TableCanvasRenderer()
425
+ canvas = renderer.render_from_file(args.json_file, args.width, args.height, output_file)
426
+
427
+ if canvas is None:
428
+ print("Failed to render canvas")
429
+ return 1
430
+
431
+ print(f"Successfully rendered {args.width}x{args.height} canvas")
432
+ return 0
433
+
434
+
435
+ if __name__ == "__main__":
436
+ exit(main())