mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
updage
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
#!/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
|
||||
|
||||
# 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 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))
|
||||
|
||||
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 = 8 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)
|
||||
|
||||
# Enhanced typography
|
||||
try:
|
||||
font_large = ImageFont.truetype("arial.ttf", 32 if is_primary else 28)
|
||||
font_small = ImageFont.truetype("arial.ttf", 11)
|
||||
except:
|
||||
font_large = ImageFont.load_default()
|
||||
font_small = ImageFont.load_default()
|
||||
|
||||
# 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
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", 18)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
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):
|
||||
"""Draw enhanced text with better typography"""
|
||||
if color is None:
|
||||
color = self.colors['text']
|
||||
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", size)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
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 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 + 15, size=12, color=self.colors['text_muted'])
|
||||
|
||||
# Value
|
||||
self.draw_text(draw, value, x + 15, y + 40, size=20, color=color)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
for issue_type, count in type_items:
|
||||
# Type bar
|
||||
bar_width = int((col_width - 40) * (count / max(self.data['find_by_type'].values())))
|
||||
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}"
|
||||
if len(label_text) > 20:
|
||||
label_text = label_text[:17] + "..."
|
||||
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)
|
||||
|
||||
# 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'])
|
||||
|
||||
# Count
|
||||
self.draw_text(draw, f"{count} issues", col3_x + 50, severity_data_y + 35,
|
||||
size=16, color=color)
|
||||
|
||||
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)
|
||||
|
||||
# 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()
|
||||
Reference in New Issue
Block a user