mirror of
https://github.com/Dvorinka/Devour.git
synced 2026-06-03 20:13:03 +00:00
update
This commit is contained in:
+133
-45
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user