Download render.py from kiaisoft/trace_src: direct link, hf CLI and curl.
- Browser
- Download file 17.9 kB
-
https://huggingface.co/kiaisoft/trace_src/resolve/main/render.py
- Command line
-
hf download hf://kiaisoft/trace_src/render.py
-
curl -L -o render.py https://huggingface.co/kiaisoft/trace_src/resolve/main/render.py
17.9 kB
| #!/usr/bin/env python3 | |
| """ | |
| Table Canvas Renderer | |
| Renders all tables from JSON data onto a single white canvas with specified dimensions | |
| """ | |
| import json | |
| import os | |
| from typing import Dict, List, Tuple, Any, Optional | |
| from PIL import Image, ImageDraw, ImageFont | |
| import argparse | |
| class TableCanvasRenderer: | |
| """Renders multiple tables from JSON data onto a canvas""" | |
| def __init__(self): | |
| """Initialize the table canvas renderer""" | |
| self.DEFAULT_FONT_SIZE = 11 | |
| self.CELL_PADDING = 4 | |
| self.DEFAULT_BORDER_WIDTH = 1 | |
| self.DEFAULT_BORDER_COLOR = "#000000" | |
| self.DEFAULT_BACKGROUND_COLOR = "#ffffff" | |
| def get_font(self, font_family: str = None, font_size: int = None, font_weight: str = "normal"): | |
| """Get PIL font object with fallbacks""" | |
| if font_size is None: | |
| font_size = self.DEFAULT_FONT_SIZE | |
| # Try to load the specified font | |
| font_paths = [] | |
| if font_family: | |
| if font_family.lower() == "ms gothic": | |
| # Common paths for MS Gothic on different systems | |
| font_paths.extend([ | |
| "C:/Windows/Fonts/msgothic.ttc", | |
| "/System/Library/Fonts/Hiragino Sans GB.ttc", | |
| "/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf" | |
| ]) | |
| else: | |
| font_paths.append(font_family) | |
| # Fallback fonts | |
| font_paths.extend([ | |
| "arial.ttf", "Arial.ttf", | |
| "/System/Library/Fonts/Arial.ttf", | |
| "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", | |
| "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" | |
| ]) | |
| for font_path in font_paths: | |
| try: | |
| font = ImageFont.truetype(font_path, font_size) | |
| # For bold text, try to find bold variant | |
| if font_weight == "bold": | |
| bold_path = font_path.replace("Regular", "Bold").replace(".ttf", "b.ttf") | |
| try: | |
| font = ImageFont.truetype(bold_path, font_size) | |
| except: | |
| pass # Use regular font if bold not available | |
| return font | |
| except (OSError, IOError): | |
| continue | |
| # Ultimate fallback to default PIL font | |
| try: | |
| return ImageFont.load_default() | |
| except: | |
| return None | |
| def hex_to_rgb(self, hex_color: str) -> Tuple[int, int, int, int]: | |
| """Convert hex color to RGBA tuple""" | |
| if not hex_color or hex_color == "transparent": | |
| return (255, 255, 255, 0) # Transparent | |
| hex_color = hex_color.lstrip('#') | |
| if len(hex_color) == 6: | |
| return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,) | |
| return (255, 255, 255, 255) # Default white | |
| def extract_tables_from_json(self, json_data: Any) -> List[Dict]: | |
| """Extract all table items from JSON data""" | |
| if isinstance(json_data, list): | |
| tables = [item for item in json_data if item.get("type") == "table"] | |
| elif isinstance(json_data, dict) and json_data.get("type") == "table": | |
| tables = [json_data] | |
| else: | |
| tables = [] | |
| return tables | |
| def calculate_cell_coordinates(self, table_properties: Dict) -> Dict[Tuple[int, int], Dict]: | |
| """Calculate coordinates for all visible cells in the table""" | |
| rows = table_properties.get("rows", 0) | |
| columns = table_properties.get("columns", 0) | |
| column_widths = table_properties.get("columnWidths", {}) | |
| row_heights = table_properties.get("rowHeights", {}) | |
| merged_cells = table_properties.get("mergedCells", {}) | |
| hidden_cells = table_properties.get("hiddenCells", {}) | |
| def get_col_width(col: int) -> int: | |
| return column_widths.get(str(col), 100) | |
| def get_row_height(row: int) -> int: | |
| return row_heights.get(str(row), 30) | |
| # Build set of cells that are covered by merged cells (excluding origin) | |
| merged_spanned_cells = set() | |
| for cell_key, merge_info in merged_cells.items(): | |
| base_row, base_col = map(int, cell_key.split('-')) | |
| rowspan = merge_info.get('rowspan', 1) | |
| colspan = merge_info.get('colspan', 1) | |
| # Add all spanned cells except the origin cell | |
| for r in range(base_row, base_row + rowspan): | |
| for c in range(base_col, base_col + colspan): | |
| if (r, c) != (base_row, base_col): | |
| merged_spanned_cells.add((r, c)) | |
| cell_coords = {} | |
| for row in range(rows): | |
| for col in range(columns): | |
| cell_key = f"{row}-{col}" | |
| # Skip hidden cells and cells covered by merges | |
| if hidden_cells.get(cell_key) or (row, col) in merged_spanned_cells: | |
| continue | |
| # Calculate position by summing previous column widths/row heights | |
| x = sum(get_col_width(c) for c in range(col)) | |
| y = sum(get_row_height(r) for r in range(row)) | |
| # Check if this cell is a merge origin | |
| if cell_key in merged_cells: | |
| merge_info = merged_cells[cell_key] | |
| colspan = merge_info.get("colspan", 1) | |
| rowspan = merge_info.get("rowspan", 1) | |
| else: | |
| colspan = 1 | |
| rowspan = 1 | |
| # Calculate cell dimensions | |
| width = sum(get_col_width(c) for c in range(col, col + colspan)) | |
| height = sum(get_row_height(r) for r in range(row, row + rowspan)) | |
| cell_coords[(row, col)] = { | |
| "x": x, | |
| "y": y, | |
| "width": width, | |
| "height": height, | |
| "colspan": colspan, | |
| "rowspan": rowspan | |
| } | |
| return cell_coords | |
| def determine_cell_borders(self, cell_data: Optional[Dict], table_properties: Dict) -> Tuple[int, int, int, int]: | |
| """Determine border visibility for each side of a cell""" | |
| # Get global border settings | |
| cell_borders = table_properties.get("cellBorders", {}) | |
| has_global_borders = cell_borders.get("all", False) | |
| # Default borders based on global setting | |
| borders = { | |
| "top": 1 if has_global_borders else 0, | |
| "bottom": 1 if has_global_borders else 0, | |
| "left": 1 if has_global_borders else 0, | |
| "right": 1 if has_global_borders else 0 | |
| } | |
| # Check for cell-specific border overrides | |
| if cell_data and "cellStyle" in cell_data: | |
| cell_style = cell_data["cellStyle"] | |
| # Border property mappings | |
| border_mappings = { | |
| "borderTopWidth": "top", | |
| "borderBottomWidth": "bottom", | |
| "borderLeftWidth": "left", | |
| "borderRightWidth": "right" | |
| } | |
| # If any border width property exists, this cell has custom borders | |
| has_custom_borders = any(key in cell_style for key in border_mappings.keys()) | |
| if has_custom_borders: | |
| # Apply custom border settings for each side | |
| for width_key, border_side in border_mappings.items(): | |
| if width_key in cell_style: | |
| # Check border width | |
| width = cell_style[width_key] | |
| has_border = width > 0 | |
| # Check border style if specified | |
| style_key = width_key.replace("Width", "Style") | |
| if style_key in cell_style: | |
| style = cell_style[style_key] | |
| if style == "none": | |
| has_border = False | |
| borders[border_side] = 1 if has_border else 0 | |
| return borders["top"], borders["bottom"], borders["left"], borders["right"] | |
| def draw_table_on_canvas(self, draw: ImageDraw.Draw, table_data: Dict): | |
| """Draw a single table on the canvas""" | |
| properties = table_data.get("properties", {}) | |
| table_x = table_data.get("x", 0) | |
| table_y = table_data.get("y", 0) | |
| table_width = table_data.get("width", properties.get("width", 800)) | |
| table_height = table_data.get("height", properties.get("height", 600)) | |
| # Calculate cell coordinates | |
| cell_coords = self.calculate_cell_coordinates(properties) | |
| # Get table styling | |
| font_size = properties.get("fontSize", self.DEFAULT_FONT_SIZE) | |
| font_family = properties.get("fontFamily", "Arial") | |
| table_bg_color = self.hex_to_rgb(properties.get("backgroundColor", self.DEFAULT_BACKGROUND_COLOR)) | |
| # Draw table background if specified | |
| if table_bg_color != (255, 255, 255, 255): # If not default white | |
| draw.rectangle([ | |
| (table_x, table_y), | |
| (table_x + table_width, table_y + table_height) | |
| ], fill=table_bg_color) | |
| # Get cell data and styling | |
| cell_data = properties.get("cellData", {}) | |
| header_bg_color = self.hex_to_rgb(properties.get("headerBackgroundColor", "#e8f7f0")) | |
| # Draw cells | |
| for (row, col), coords in cell_coords.items(): | |
| cell_key = f"{row}-{col}" | |
| current_cell_data = cell_data.get(cell_key, {}) | |
| # Calculate cell position on canvas | |
| cell_x = table_x + coords["x"] | |
| cell_y = table_y + coords["y"] | |
| cell_w = coords["width"] | |
| cell_h = coords["height"] | |
| # Get cell styling | |
| cell_style = current_cell_data.get("cellStyle", {}) | |
| cell_bg_color = cell_style.get("backgroundColor") | |
| # Use header background for header rows if no specific background | |
| if row == 0 and not cell_bg_color: | |
| cell_bg_color = properties.get("headerBackgroundColor", "#e8f7f0") | |
| elif not cell_bg_color: | |
| cell_bg_color = properties.get("backgroundColor", self.DEFAULT_BACKGROUND_COLOR) | |
| # Draw cell background | |
| if cell_bg_color and cell_bg_color != "transparent": | |
| bg_color = self.hex_to_rgb(cell_bg_color) | |
| draw.rectangle([ | |
| (cell_x, cell_y), | |
| (cell_x + cell_w, cell_y + cell_h) | |
| ], fill=bg_color) | |
| # Draw cell borders | |
| top, bottom, left, right = self.determine_cell_borders(current_cell_data, properties) | |
| border_color = self.hex_to_rgb( | |
| cell_style.get("borderBottomColor", | |
| cell_style.get("borderColor", self.DEFAULT_BORDER_COLOR)) | |
| )[:3] # Remove alpha for line drawing | |
| border_width = max(1, cell_style.get("borderWidth", self.DEFAULT_BORDER_WIDTH)) | |
| if top: | |
| draw.line([(cell_x, cell_y), (cell_x + cell_w, cell_y)], | |
| fill=border_color, width=border_width) | |
| if bottom: | |
| draw.line([(cell_x, cell_y + cell_h), (cell_x + cell_w, cell_y + cell_h)], | |
| fill=border_color, width=border_width) | |
| if left: | |
| draw.line([(cell_x, cell_y), (cell_x, cell_y + cell_h)], | |
| fill=border_color, width=border_width) | |
| if right: | |
| draw.line([(cell_x + cell_w, cell_y), (cell_x + cell_w, cell_y + cell_h)], | |
| fill=border_color, width=border_width) | |
| # Draw cell text | |
| cell_value = current_cell_data.get("value", "") | |
| if cell_value: | |
| # Get text styling | |
| text_color = self.hex_to_rgb( | |
| cell_style.get("color", | |
| properties.get("color", "#000000")) | |
| )[:3] # Remove alpha | |
| font_weight = cell_style.get("fontWeight", "normal") | |
| if row == 0: # Header row | |
| font_weight = properties.get("headerFontWeight", "bold") | |
| text_align = cell_style.get("textAlign", "left") | |
| cell_font_size = cell_style.get("fontSize", font_size) | |
| # Get font | |
| font = self.get_font(font_family, cell_font_size, font_weight) | |
| # Calculate text position | |
| try: | |
| text_bbox = draw.textbbox((0, 0), str(cell_value), font=font) | |
| text_width = text_bbox[2] - text_bbox[0] | |
| text_height = text_bbox[3] - text_bbox[1] | |
| except: | |
| # Fallback for older PIL versions | |
| text_width, text_height = draw.textsize(str(cell_value), font=font) | |
| # Position based on alignment | |
| if text_align == "center": | |
| text_x = cell_x + (cell_w - text_width) // 2 | |
| elif text_align == "right": | |
| text_x = cell_x + cell_w - text_width - self.CELL_PADDING | |
| else: # left | |
| text_x = cell_x + self.CELL_PADDING | |
| text_y = cell_y + (cell_h - text_height) // 2 | |
| # Draw text | |
| draw.text((text_x, text_y), str(cell_value), | |
| fill=text_color, font=font) | |
| def render_tables_to_canvas(self, json_data: Any, canvas_width: int, canvas_height: int) -> Image.Image: | |
| """ | |
| Render all tables from JSON data onto a white canvas | |
| Args: | |
| json_data: JSON data containing table definitions | |
| canvas_width: Width of the canvas | |
| canvas_height: Height of the canvas | |
| Returns: | |
| PIL Image with all tables rendered | |
| """ | |
| # Create white canvas | |
| canvas = Image.new('RGBA', (canvas_width, canvas_height), (255, 255, 255, 255)) | |
| draw = ImageDraw.Draw(canvas) | |
| # Extract all tables from JSON | |
| tables = self.extract_tables_from_json(json_data) | |
| if not tables: | |
| print("No tables found in JSON data") | |
| return canvas | |
| print(f"Found {len(tables)} table(s) to render") | |
| # Draw each table on the canvas | |
| for i, table_data in enumerate(tables): | |
| try: | |
| table_id = table_data.get("id", f"table_{i}") | |
| print(f" Drawing table {i+1}/{len(tables)} (id: {table_id})") | |
| self.draw_table_on_canvas(draw, table_data) | |
| except Exception as e: | |
| print(f" Error drawing table {i+1}: {e}") | |
| continue | |
| print("Canvas rendering complete") | |
| return canvas | |
| def render_from_file(self, json_file: str, canvas_width: int, canvas_height: int, output_file: str = None) -> Image.Image: | |
| """ | |
| Render tables from JSON file to canvas | |
| Args: | |
| json_file: Path to JSON file | |
| canvas_width: Width of the canvas | |
| canvas_height: Height of the canvas | |
| output_file: Optional path to save the rendered canvas | |
| Returns: | |
| PIL Image with all tables rendered | |
| """ | |
| # Read JSON data | |
| try: | |
| with open(json_file, 'r', encoding='utf-8') as f: | |
| json_data = json.load(f) | |
| except Exception as e: | |
| print(f"Error reading JSON file: {e}") | |
| return None | |
| # Render to canvas | |
| canvas = self.render_tables_to_canvas(json_data, canvas_width, canvas_height) | |
| # Save if output file specified | |
| if output_file: | |
| try: | |
| canvas.save(output_file) | |
| print(f"Canvas saved to: {output_file}") | |
| except Exception as e: | |
| print(f"Error saving canvas: {e}") | |
| return canvas | |
| def main(): | |
| """Main function for command line usage""" | |
| parser = argparse.ArgumentParser(description="Render tables from JSON data to canvas") | |
| parser.add_argument("json_file", help="Path to JSON file containing table data") | |
| parser.add_argument("width", type=int, help="Canvas width in pixels") | |
| parser.add_argument("height", type=int, help="Canvas height in pixels") | |
| parser.add_argument("--output", "-o", help="Output image file path (optional)") | |
| args = parser.parse_args() | |
| # Validate arguments | |
| if args.width <= 0 or args.height <= 0: | |
| print("Canvas width and height must be positive integers") | |
| return | |
| if not os.path.exists(args.json_file): | |
| print(f"JSON file not found: {args.json_file}") | |
| return | |
| # Generate output filename if not provided | |
| output_file = args.output | |
| if not output_file: | |
| base_name = os.path.splitext(os.path.basename(args.json_file))[0] | |
| output_file = f"{base_name}_canvas_{args.width}x{args.height}.png" | |
| # Create renderer and process file | |
| renderer = TableCanvasRenderer() | |
| canvas = renderer.render_from_file(args.json_file, args.width, args.height, output_file) | |
| if canvas is None: | |
| print("Failed to render canvas") | |
| return 1 | |
| print(f"Successfully rendered {args.width}x{args.height} canvas") | |
| return 0 | |
| if __name__ == "__main__": | |
| exit(main()) |