Files
Devour/cmd/banner_generator/main.py
T
Tomas Dvorak 898a3c303f update
2026-02-24 10:33:59 +01:00

703 lines
31 KiB
Python

#!/usr/bin/env python3
"""
Modern PNG Banner Generator for Devour Scorecards
Creates beautiful dark-themed banners with proper typography and data visualization
Consistent with Go implementation design patterns
"""
import os
import sys
from PIL import Image, ImageDraw, ImageFont
import math
import argparse
class ModernBannerGenerator:
def __init__(self, data):
self.data = data
self.fonts = self._init_fonts()
# Devour brand colors - consistent with Go theme
self.colors = {
# Dark theme backgrounds
'bg_start': (15, 23, 42), # #0f172a - deep slate
'bg_end': (30, 41, 59), # #1e293b - elevated surface
'card': (30, 41, 59), # #1e293b - card surface
'card_alt': (51, 65, 85), # #334155 - alternate card
'border': (71, 85, 105), # #475569 - subtle border
'border_subtle': (51, 65, 85), # #334155 - subtle border
# Text colors
'text': (248, 250, 252), # #f8f9fc - pure white
'text_muted': (148, 163, 184), # #94a3b8 - soft gray
'text_dim': (100, 116, 139), # #64748b - dimmed gray
# Devour brand accent colors
'orange': (251, 146, 60), # #fb923c - devour orange
'red': (239, 68, 68), # #ef4444 - bright red
'yellow': (251, 191, 36), # #fbbf24 - bright yellow
'amber': (251, 191, 36), # #fbbf24 - amber
# Score grade colors - vibrant for dark theme
'score_a': (52, 211, 153), # #34d399 - bright emerald
'score_b': (34, 211, 238), # #22d3ee - bright cyan
'score_c': (251, 191, 36), # #fbbf24 - bright amber
'score_d': (251, 146, 60), # #fb923c - bright orange
'score_f': (248, 113, 113), # #f87171 - bright red
# Muted score colors for secondary metrics
'score_a_muted': (74, 222, 128), # #4ade80 - muted emerald
'score_b_muted': (125, 185, 255), # #7db9ff - muted cyan
'score_c_muted': (217, 119, 6), # #d97706 - muted amber
'score_d_muted': (234, 88, 12), # #ea580c - muted orange
'score_f_muted': (220, 38, 38), # #dc2626 - muted red
# Severity colors - high contrast
'severity_t1': (96, 165, 250), # #60a5fa - bright blue
'severity_t2': (251, 191, 36), # #fbbf24 - bright amber
'severity_t3': (251, 146, 60), # #fb923c - bright orange
'severity_t4': (248, 113, 113), # #f87171 - bright red
}
def _init_fonts(self):
"""Initialize font candidates and cache."""
# Prefer widely-available fonts on Linux/macOS/Windows.
font_candidates = {
"regular": [
"arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/Library/Fonts/Arial.ttf",
],
"bold": [
"arialbd.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/System/Library/Fonts/Supplemental/Arial Bold.ttf",
"/Library/Fonts/Arial Bold.ttf",
],
}
return {
"candidates": font_candidates,
"cache": {},
}
def get_font(self, size, weight="regular"):
"""Get a cached font or fall back to the default."""
key = (size, weight)
if key in self.fonts["cache"]:
return self.fonts["cache"][key]
for path in self.fonts["candidates"].get(weight, []):
try:
font = ImageFont.truetype(path, size)
self.fonts["cache"][key] = font
return font
except:
continue
font = ImageFont.load_default()
self.fonts["cache"][key] = font
return font
def get_score_color(self, score, muted=False):
if score >= 90:
return self.colors['score_a_muted'] if muted else self.colors['score_a']
elif score >= 70:
return self.colors['score_b_muted'] if muted else self.colors['score_b']
elif score >= 50:
return self.colors['score_c_muted'] if muted else self.colors['score_c']
elif score >= 30:
return self.colors['score_d_muted'] if muted else self.colors['score_d']
else:
return self.colors['score_f_muted'] if muted else self.colors['score_f']
def get_grade_color(self, grade, muted=False):
grade_scores = {'A': 95, 'B': 80, 'C': 60, 'D': 40, 'F': 20}
score = grade_scores.get(grade.upper(), 50)
return self.get_score_color(score, muted)
def get_grade_score(self, grade):
"""Convert grade to numeric score for visualization"""
grade_scores = {'A': 95, 'B': 80, 'C': 60, 'D': 40, 'F': 20}
return grade_scores.get(grade.upper(), 50)
def draw_gradient_background(self, img, width, height):
"""Draw gradient background"""
for y in range(height):
ratio = y / height
r = int(self.colors['bg_start'][0] * (1 - ratio) + self.colors['bg_end'][0] * ratio)
g = int(self.colors['bg_start'][1] * (1 - ratio) + self.colors['bg_end'][1] * ratio)
b = int(self.colors['bg_start'][2] * (1 - ratio) + self.colors['bg_end'][2] * ratio)
for x in range(width):
img.putpixel((x, y), (r, g, b))
# Add subtle radial glows for depth
self.draw_glow(img, width * 0.15, height * 0.2, 220, (71, 85, 105), 40)
self.draw_glow(img, width * 0.85, height * 0.75, 260, (251, 146, 60), 35)
def draw_glow(self, img, cx, cy, radius, color, max_alpha):
"""Draw a soft radial glow."""
draw = ImageDraw.Draw(img)
steps = 12
for i in range(steps):
r = radius - (radius * i / steps)
alpha = int(max_alpha * (1 - i / steps))
draw.ellipse(
[(cx - r, cy - r), (cx + r, cy + r)],
fill=(*color, alpha),
)
def draw_glass_card(self, draw, x, y, width, height, border_radius=12, use_alt=False):
"""Draw glass morphism card with enhanced effects"""
card_color = self.colors['card_alt'] if use_alt else self.colors['card']
# Draw rounded rectangle with shadow effect
draw.rounded_rectangle(
[(x+2, y+2), (x + width+2, y + height+2)],
border_radius,
fill=(0, 0, 0, 50) # Subtle shadow
)
# Main card
draw.rounded_rectangle(
[(x, y), (x + width, y + height)],
border_radius,
fill=(*card_color, 240),
outline=(*self.colors['border'], 180),
width=1
)
# Add subtle gradient overlay for depth
overlay_height = height // 3
for py in range(overlay_height):
alpha = int(15 * (1 - py / overlay_height))
for px in range(width):
if px > border_radius and px < width - border_radius:
try:
r, g, b, a = draw.im.getpixel((x + px, y + py))
draw.im.putpixel((x + px, y + py), (r, g, b, min(a + alpha, 255)))
except:
pass
def draw_score_circle(self, draw, cx, cy, radius, score, label="OVERALL", is_primary=True):
"""Draw enhanced circular score visualization"""
# Background circle with subtle border
draw.ellipse([(cx-radius-2, cy-radius-2), (cx+radius+2, cy+radius+2)],
fill=(*self.colors['border'], 100))
draw.ellipse([(cx-radius, cy-radius), (cx+radius, cy+radius)],
fill=self.colors['card'], outline=self.colors['border'])
# Progress arc with enhanced styling
if score > 0:
score_color = self.get_score_color(score, muted=not is_primary)
percentage = score / 100.0
# Draw background arc
draw.arc([(cx-radius+4, cy-radius+4), (cx+radius-4, cy+radius-4)],
-90, 270, fill=self.colors['border_subtle'], width=6)
# Draw progress arc
start_angle = -90
end_angle = start_angle + (360 * percentage)
arc_width = 9 if is_primary else 6
draw.arc([(cx-radius+4, cy-radius+4), (cx+radius-4, cy+radius-4)],
start_angle, end_angle,
fill=score_color, width=arc_width)
# Inner glow ring
if is_primary:
draw.arc([(cx-radius+10, cy-radius+10), (cx+radius-10, cy+radius-10)],
start_angle, end_angle, fill=score_color, width=2)
# Enhanced typography
font_large = self.get_font(34 if is_primary else 28, weight="bold")
font_small = self.get_font(11, weight="regular")
# Score text
score_text = f"{int(score)}%"
bbox = draw.textbbox((0, 0), score_text, font=font_large)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
text_color = self.colors['text'] if is_primary else self.colors['text_muted']
draw.text((cx - text_width//2, cy - text_height//2 - 2), score_text,
fill=text_color, font=font_large)
# Label
label_bbox = draw.textbbox((0, 0), label, font=font_small)
label_width = label_bbox[2] - label_bbox[0]
draw.text((cx - label_width//2, cy + radius + 15), label,
fill=self.colors['text_dim'], font=font_small)
def draw_grade_badge(self, draw, x, y, grade):
"""Draw enhanced grade badge"""
grade_color = self.get_grade_color(grade)
# Badge background with shadow
badge_width, badge_height = 55, 28
# Shadow
draw.rounded_rectangle([(x+2, y+2), (x + badge_width+2, y + badge_height+2)],
6, fill=(0, 0, 0, 60))
# Main badge
draw.rounded_rectangle([(x, y), (x + badge_width, y + badge_height)],
6, fill=grade_color, outline=self.colors['border'])
# Grade text with better typography
font = self.get_font(18, weight="bold")
bbox = draw.textbbox((0, 0), grade, font=font)
text_width = bbox[2] - bbox[0]
text_height = bbox[3] - bbox[1]
draw.text((x + badge_width//2 - text_width//2, y + badge_height//2 - text_height//2 + 1),
grade, fill=(255, 255, 255), font=font)
def draw_text(self, draw, text, x, y, size=14, color=None, centered=False, max_width=None, min_size=9, weight="regular"):
"""Draw enhanced text with better typography"""
if color is None:
color = self.colors['text']
font = self.get_font(size, weight=weight)
if max_width is not None:
font = self.fit_font(draw, text, font, max_width, min_size=min_size, weight=weight)
if centered:
bbox = draw.textbbox((0, 0), text, font=font)
text_width = bbox[2] - bbox[0]
x = x - text_width // 2
draw.text((x, y), text, fill=color, font=font)
def fit_font(self, draw, text, font, max_width, min_size=9, weight="regular"):
"""Shrink font until text fits max width."""
if font == ImageFont.load_default():
return font
size = font.size if hasattr(font, "size") else min_size
current = font
while size > min_size:
bbox = draw.textbbox((0, 0), text, font=current)
if (bbox[2] - bbox[0]) <= max_width:
return current
size -= 1
current = self.get_font(size, weight=weight)
return current
def truncate_text(self, draw, text, font, max_width):
"""Truncate text with ellipsis to fit width."""
if max_width <= 0:
return ""
if draw.textbbox((0, 0), text, font=font)[2] <= max_width:
return text
ellipsis = "..."
for i in range(len(text), 0, -1):
candidate = text[:i] + ellipsis
if draw.textbbox((0, 0), candidate, font=font)[2] <= max_width:
return candidate
return ellipsis
def draw_metric_card(self, draw, x, y, width, height, title, value, color):
"""Draw metric card"""
self.draw_glass_card(draw, x, y, width, height)
# Title
self.draw_text(draw, title, x + 15, y + 14, size=12, color=self.colors['text_muted'])
# Value
self.draw_text(draw, value, x + 15, y + 38, size=20, color=color, weight="bold")
def draw_severity_bars(self, draw, x, y, width, height, find_by_tier):
"""Draw enhanced severity bars"""
tiers = [
("T1", "Auto-fixable", self.colors['severity_t1']),
("T2", "Quick Manual", self.colors['severity_t2']),
("T3", "Needs Judgment", self.colors['severity_t3']),
("T4", "Major Refactor", self.colors['severity_t4']),
]
bar_width = (width - 40) // len(tiers)
max_count = max(find_by_tier.values()) if find_by_tier else 1
for i, (tier, label, color) in enumerate(tiers):
bar_x = x + 20 + i * (bar_width + 10)
count = find_by_tier.get(tier, 0)
# Bar background with rounded effect
bg_height = height - 35
draw.rounded_rectangle([(bar_x, y), (bar_x + bar_width, y + bg_height)],
4, fill=self.colors['card'], outline=self.colors['border_subtle'])
# Bar fill with gradient effect
if count > 0:
fill_height = int(bg_height * (count / max_count))
fill_y = y + bg_height - fill_height
# Main fill
draw.rounded_rectangle([(bar_x, fill_y), (bar_x + bar_width, y + bg_height)],
4, fill=color)
# Highlight at top
if fill_height > 4:
draw.rectangle([(bar_x, fill_y), (bar_x + bar_width, fill_y + 2)],
fill=tuple(min(c + 30, 255) for c in color))
# Enhanced label
self.draw_text(draw, f"{tier}", bar_x + bar_width//2, y + height - 30,
size=10, color=self.colors['text_muted'], centered=True)
self.draw_text(draw, f"{count}", bar_x + bar_width//2, y + height - 18,
size=9, color=self.colors['text_dim'], centered=True)
def generate_compact_banner(self, output_path):
"""Generate compact banner with enhanced design"""
width, height = 1200, 400 # Consistent with Go implementation
img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Enhanced gradient background
self.draw_gradient_background(img, width, height)
# Main content area with better spacing
content_x, content_y = 60, 60
content_width, content_height = width - 120, height - 120
# Enhanced glass card
self.draw_glass_card(draw, content_x, content_y, content_width, content_height, border_radius=16)
# Three-circle layout matching Go implementation
circle_radius = 70
circle_y = content_y + content_height // 2
circle_spacing = 40
total_circles_width = 3*circle_radius*2 + 2*circle_spacing
start_x = content_x + (content_width-total_circles_width)//2 + circle_radius
# Circle 1: Overall Score (primary)
self.draw_score_circle(draw, start_x, circle_y, circle_radius,
self.data['overall_score'], "OVERALL", is_primary=True)
# Circle 2: Strict Score (secondary)
x2 = start_x + circle_radius*2 + circle_spacing
self.draw_score_circle(draw, x2, circle_y, circle_radius,
self.data['strict_score'], "STRICT", is_primary=False)
# Circle 3: Grade (special)
x3 = x2 + circle_radius*2 + circle_spacing
grade_score = self.get_grade_score(self.data['grade'])
self.draw_score_circle(draw, x3, circle_y, circle_radius,
grade_score, self.data['grade'], is_primary=True)
# Grade badge positioned above circle 3
self.draw_grade_badge(draw, x3 - 27, circle_y - circle_radius - 20, self.data['grade'])
# Enhanced header section
header_y = content_y + 20
self.draw_text(draw, "DEVOUR SCORE", content_x + content_width//2, header_y,
size=20, color=self.colors['text'], centered=True, weight="bold")
# Project info
project_name = self.data['project_name']
version_text = f"v{self.data['version']}" if self.data['version'] else "latest"
project_text = f"{project_name} {version_text}"
self.draw_text(draw, project_text, content_x + content_width//2, header_y + 25,
size=14, color=self.colors['text_muted'], centered=True, max_width=content_width - 120)
# Timestamp
time_text = self.data.get('timestamp', 'Today')
self.draw_text(draw, time_text, content_x + content_width//2,
content_y + content_height - 25,
size=11, color=self.colors['text_dim'], centered=True)
# Enhanced findings section
findings_y = circle_y + circle_radius + 50
findings_bg_height = content_height - (findings_y - content_y) - 20
if findings_bg_height > 0:
self.draw_glass_card(draw, content_x + 20, findings_y,
content_width - 40, findings_bg_height,
border_radius=8, use_alt=True)
# Findings metrics in 3 columns
findings_total = self.data['total_findings']
findings_open = self.data['open_findings']
findings_closed = findings_total - findings_open
col_width = (content_width - 80) // 3
col_x = content_x + 40
metrics_y = findings_y + 15
# Total findings
self.draw_text(draw, str(findings_total), col_x + col_width//2, metrics_y,
size=18, color=self.colors['text'], centered=True, weight="bold")
self.draw_text(draw, "TOTAL", col_x + col_width//2, metrics_y + 22,
size=10, color=self.colors['text_muted'], centered=True)
# Open findings
self.draw_text(draw, str(findings_open), col_x + col_width + col_width//2, metrics_y,
size=18, color=self.colors['orange'], centered=True, weight="bold")
self.draw_text(draw, "OPEN", col_x + col_width + col_width//2, metrics_y + 22,
size=10, color=self.colors['text_muted'], centered=True)
# Resolved findings
self.draw_text(draw, str(findings_closed), col_x + 2*col_width + col_width//2, metrics_y,
size=18, color=self.colors['score_a'], centered=True, weight="bold")
self.draw_text(draw, "RESOLVED", col_x + 2*col_width + col_width//2, metrics_y + 22,
size=10, color=self.colors['text_muted'], centered=True)
# Save image with high quality
img.save(output_path, "PNG", optimize=True)
return output_path
def generate_detailed_banner(self, output_path):
"""Generate detailed banner with 3-grid data layout"""
width, height = 1400, 600 # Slightly taller for grid layout
img = Image.new('RGBA', (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Enhanced gradient background
self.draw_gradient_background(img, width, height)
# Header section
header_y = 30
self.draw_text(draw, f"{self.data['project_name']} Quality Report",
width//2, header_y, size=28, color=self.colors['text'], centered=True, weight="bold", max_width=width - 80)
version_text = f"v{self.data['version']}" if self.data['version'] else "latest"
self.draw_text(draw, version_text, width//2, header_y + 35,
size=16, color=self.colors['text_muted'], centered=True)
# Main score section - single prominent score
score_section_y = header_y + 80
score_x, score_y = 150, score_section_y + 60
# Large overall score circle
self.draw_score_circle(draw, score_x, score_y, 70,
self.data['overall_score'], "OVERALL", is_primary=True)
# Grade badge above score circle
self.draw_grade_badge(draw, score_x - 25, score_y - 55, self.data['grade'])
# Score details
score_details_y = score_y + 100
self.draw_text(draw, f"Overall: {int(self.data['overall_score'])}%",
score_x, score_details_y, size=20,
color=self.get_score_color(self.data['overall_score']), centered=True, weight="bold")
self.draw_text(draw, f"Strict: {int(self.data['strict_score'])}%",
score_x, score_details_y + 25, size=16,
color=self.get_score_color(self.data['strict_score'], muted=True), centered=True)
# Three-column grid for detailed data
grid_start_y = header_y + 80
grid_start_x = 350
grid_width = 1000
grid_height = 450
col_width = 320
col_spacing = 20
# Column 1: Score Breakdown
col1_x = grid_start_x
self.draw_glass_card(draw, col1_x, grid_start_y, col_width, grid_height, border_radius=12)
# Column 1 Header
self.draw_text(draw, "Score Breakdown", col1_x + col_width//2, grid_start_y + 20,
size=18, color=self.colors['text'], centered=True, weight="bold")
# Column 1 Data
score_data = [
("Overall Score", f"{int(self.data['overall_score'])}%", self.get_score_color(self.data['overall_score'])),
("Strict Score", f"{int(self.data['strict_score'])}%", self.get_score_color(self.data['strict_score'], muted=True)),
("Grade", self.data['grade'], self.get_grade_color(self.data['grade'])),
]
data_y = grid_start_y + 60
for label, value, color in score_data:
# Draw metric card
self.draw_glass_card(draw, col1_x + 10, data_y, col_width - 20, 70, border_radius=8, use_alt=True)
# Label
self.draw_text(draw, label, col1_x + 20, data_y + 10,
size=12, color=self.colors['text_muted'])
# Value
self.draw_text(draw, value, col1_x + col_width//2, data_y + 35,
size=24, color=color, centered=True, weight="bold")
data_y += 80
# Column 2: Findings by Type
col2_x = col1_x + col_width + col_spacing
self.draw_glass_card(draw, col2_x, grid_start_y, col_width, grid_height, border_radius=12)
# Column 2 Header
self.draw_text(draw, "Findings by Type", col2_x + col_width//2, grid_start_y + 20,
size=18, color=self.colors['text'], centered=True, weight="bold")
# Column 2 Data - Top finding types
type_data_y = grid_start_y + 60
type_items = list(self.data['find_by_type'].items())[:6] # Top 6 types
max_type_count = max(self.data['find_by_type'].values()) if self.data['find_by_type'] else 1
if not type_items:
self.draw_text(draw, "No findings", col2_x + col_width//2, grid_start_y + 110,
size=14, color=self.colors['text_dim'], centered=True)
for issue_type, count in type_items:
# Type bar
bar_width = int((col_width - 40) * (count / max_type_count))
bar_height = 22
# Bar background
draw.rounded_rectangle([(col2_x + 20, type_data_y), (col2_x + col_width - 20, type_data_y + bar_height)],
4, fill=self.colors['card'], outline=self.colors['border_subtle'])
# Bar fill
draw.rounded_rectangle([(col2_x + 20, type_data_y), (col2_x + 20 + bar_width, type_data_y + bar_height)],
4, fill=self.colors['orange'])
# Type label
label_text = f"{issue_type}".replace("_", " ")
font_label = self.get_font(11, weight="regular")
label_text = self.truncate_text(draw, label_text, font_label, col_width - 90)
self.draw_text(draw, label_text, col2_x + 25, type_data_y + 2,
size=11, color=self.colors['text_muted'])
# Count
self.draw_text(draw, f"({count})", col2_x + col_width - 35, type_data_y + 2,
size=10, color=self.colors['text_dim'])
type_data_y += bar_height + 12
# Column 3: Findings by Severity
col3_x = col2_x + col_width + col_spacing
self.draw_glass_card(draw, col3_x, grid_start_y, col_width, grid_height, border_radius=12)
# Column 3 Header
self.draw_text(draw, "Issues by Severity", col3_x + col_width//2, grid_start_y + 20,
size=18, color=self.colors['text'], centered=True, weight="bold")
# Column 3 Data - Severity breakdown
severity_data_y = grid_start_y + 60
severity_items = [
("Critical (T4)", self.data['find_by_tier'].get('T4', 0), self.colors['score_f']),
("High (T3)", self.data['find_by_tier'].get('T3', 0), self.colors['score_d']),
("Medium (T2)", self.data['find_by_tier'].get('T2', 0), self.colors['score_c']),
("Low (T1)", self.data['find_by_tier'].get('T1', 0), self.colors['score_a']),
]
for severity_name, count, color in severity_items:
# Severity card
self.draw_glass_card(draw, col3_x + 10, severity_data_y, col_width - 20, 60, border_radius=8, use_alt=True)
# Severity indicator
indicator_size = 12
draw.ellipse([(col3_x + 25, severity_data_y + 24),
(col3_x + 25 + indicator_size, severity_data_y + 24 + indicator_size)],
fill=color)
# Severity name
self.draw_text(draw, severity_name, col3_x + 50, severity_data_y + 15,
size=14, color=self.colors['text'], max_width=col_width - 70)
# Count
self.draw_text(draw, f"{count} issues", col3_x + 50, severity_data_y + 35,
size=16, color=color, weight="bold")
severity_data_y += 70
# Summary metrics at bottom
summary_y = grid_start_y + grid_height + 20
summary_metrics = [
("Total Findings", str(self.data['total_findings']), self.colors['text']),
("Open Issues", str(self.data['open_findings']), self.colors['orange']),
("Resolved", str(self.data['total_findings'] - self.data['open_findings']), self.colors['score_a']),
]
metrics_width = 200
metrics_spacing = 30
total_metrics_width = len(summary_metrics) * metrics_width + (len(summary_metrics) - 1) * metrics_spacing
summary_start_x = (width - total_metrics_width) // 2
for i, (label, value, color) in enumerate(summary_metrics):
metric_x = summary_start_x + i * (metrics_width + metrics_spacing)
# Summary card
self.draw_glass_card(draw, metric_x, summary_y, metrics_width, 50, border_radius=8, use_alt=True)
# Value
self.draw_text(draw, value, metric_x + metrics_width//2, summary_y + 10,
size=18, color=color, centered=True, weight="bold")
# Label
self.draw_text(draw, label, metric_x + metrics_width//2, summary_y + 30,
size=10, color=self.colors['text_muted'], centered=True)
# Footer with timestamp
footer_y = height - 20
time_text = self.data.get('timestamp', 'Generated today')
self.draw_text(draw, time_text, width//2, footer_y,
size=10, color=self.colors['text_dim'], centered=True)
# Save image with high quality
img.save(output_path, "PNG", optimize=True)
return output_path
def main():
# Enhanced sample data matching Go implementation
from datetime import datetime
data = {
'project_name': 'Devour',
'version': '2.0.0',
'overall_score': 65.0,
'strict_score': 62.0,
'grade': 'C',
'total_findings': 7,
'open_findings': 7,
'timestamp': datetime.now().strftime('%B %d, %Y'),
'find_by_tier': {'T1': 1, 'T2': 3, 'T3': 1, 'T4': 2},
'find_by_type': {
'complexity': 1, 'naming': 1, 'duplication': 1,
'security': 1, 'unused_import': 1, 'dead_code': 1, 'god_component': 1
}
}
parser = argparse.ArgumentParser(description='Generate modern PNG scorecard banners')
parser.add_argument('--compact', action='store_true', help='Generate compact banner')
parser.add_argument('--detailed', action='store_true', help='Generate detailed banner')
parser.add_argument('--output', default='lighthouse_scorecard.png', help='Output filename')
parser.add_argument('--dark-only', action='store_true', default=True, help='Generate dark theme only (default)')
args = parser.parse_args()
generator = ModernBannerGenerator(data)
if args.compact:
output_path = args.output.replace('.png', '_compact_dark.png')
generator.generate_compact_banner(output_path)
print(f"✓ Generated {output_path}")
elif args.detailed:
output_path = args.output.replace('.png', '_detailed_dark.png')
generator.generate_detailed_banner(output_path)
print(f"✓ Generated {output_path}")
else:
# Generate both by default with dark theme
compact_path = args.output.replace('.png', '_compact_dark.png')
detailed_path = args.output.replace('.png', '_detailed_dark.png')
generator.generate_compact_banner(compact_path)
generator.generate_detailed_banner(detailed_path)
print(f"✓ Generated {compact_path}")
print(f"✓ Generated {detailed_path}")
print(f"\n🎨 Enhanced Dark Theme Scorecards")
print(f"📊 Project: {data['project_name']} v{data['version']}")
print(f"⚡ Overall Score: {data['overall_score']:.0f}% ({data['grade']})")
print(f"🔒 Strict Score: {data['strict_score']:.0f}%")
print(f"📋 Total Findings: {data['total_findings']} ({data['open_findings']} open)")
print(f"🎨 Features: Modern dark theme, enhanced typography, glass morphism effects")
if __name__ == "__main__":
main()