File size: 17,942 Bytes
5cf94fe |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 |
#!/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()) |