This commit is contained in:
Tomas Dvorak
2026-02-24 10:33:59 +01:00
parent 409acd2e08
commit 898a3c303f
1374 changed files with 290409 additions and 29187 deletions
+133 -45
View File
@@ -14,6 +14,7 @@ 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 = {
@@ -56,6 +57,49 @@ class ModernBannerGenerator:
'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:
@@ -89,6 +133,22 @@ class ModernBannerGenerator:
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"""
@@ -125,9 +185,9 @@ class ModernBannerGenerator:
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)],
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)],
draw.ellipse([(cx-radius, cy-radius), (cx+radius, cy+radius)],
fill=self.colors['card'], outline=self.colors['border'])
# Progress arc with enhanced styling
@@ -136,25 +196,26 @@ class ModernBannerGenerator:
percentage = score / 100.0
# Draw background arc
draw.arc([(cx-radius+4, cy-radius+4), (cx+radius-4, cy+radius-4)],
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
arc_width = 9 if is_primary else 6
draw.arc([(cx-radius+4, cy-radius+4), (cx+radius-4, cy+radius-4)],
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
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()
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)}%"
@@ -163,14 +224,14 @@ class ModernBannerGenerator:
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,
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,
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):
@@ -185,14 +246,11 @@ class ModernBannerGenerator:
6, fill=(0, 0, 0, 60))
# Main badge
draw.rounded_rectangle([(x, y), (x + badge_width, y + badge_height)],
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()
font = self.get_font(18, weight="bold")
bbox = draw.textbbox((0, 0), grade, font=font)
text_width = bbox[2] - bbox[0]
@@ -201,15 +259,14 @@ class ModernBannerGenerator:
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):
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']
try:
font = ImageFont.truetype("arial.ttf", size)
except:
font = ImageFont.load_default()
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)
@@ -217,16 +274,43 @@ class ModernBannerGenerator:
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 + 15, size=12, color=self.colors['text_muted'])
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 + 40, size=20, color=color)
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"""
@@ -313,18 +397,18 @@ class ModernBannerGenerator:
# 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)
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)
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,
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)
@@ -347,19 +431,19 @@ class ModernBannerGenerator:
# Total findings
self.draw_text(draw, str(findings_total), col_x + col_width//2, metrics_y,
size=18, color=self.colors['text'], centered=True)
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)
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)
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)
@@ -379,7 +463,7 @@ class ModernBannerGenerator:
# 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)
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,
@@ -399,8 +483,8 @@ class ModernBannerGenerator:
# 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)
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)
@@ -419,7 +503,7 @@ class ModernBannerGenerator:
# 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)
size=18, color=self.colors['text'], centered=True, weight="bold")
# Column 1 Data
score_data = [
@@ -439,7 +523,7 @@ class ModernBannerGenerator:
# Value
self.draw_text(draw, value, col1_x + col_width//2, data_y + 35,
size=24, color=color, centered=True)
size=24, color=color, centered=True, weight="bold")
data_y += 80
@@ -449,15 +533,19 @@ class ModernBannerGenerator:
# 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)
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(self.data['find_by_type'].values())))
bar_width = int((col_width - 40) * (count / max_type_count))
bar_height = 22
# Bar background
@@ -469,9 +557,9 @@ class ModernBannerGenerator:
4, fill=self.colors['orange'])
# Type label
label_text = f"{issue_type}"
if len(label_text) > 20:
label_text = label_text[:17] + "..."
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'])
@@ -487,7 +575,7 @@ class ModernBannerGenerator:
# 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)
size=18, color=self.colors['text'], centered=True, weight="bold")
# Column 3 Data - Severity breakdown
severity_data_y = grid_start_y + 60
@@ -510,11 +598,11 @@ class ModernBannerGenerator:
# Severity name
self.draw_text(draw, severity_name, col3_x + 50, severity_data_y + 15,
size=14, color=self.colors['text'])
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)
size=16, color=color, weight="bold")
severity_data_y += 70
@@ -539,7 +627,7 @@ class ModernBannerGenerator:
# Value
self.draw_text(draw, value, metric_x + metrics_width//2, summary_y + 10,
size=18, color=color, centered=True)
size=18, color=color, centered=True, weight="bold")
# Label
self.draw_text(draw, label, metric_x + metrics_width//2, summary_y + 30,