Spaces:
Sleeping
Sleeping
File size: 6,758 Bytes
fd027e9 | 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 | import os
from PIL import Image, ImageDraw, ImageFont, ImageFilter
from typing import Optional
from models import LabReportData
class VisualRenderer:
def __init__(self, assets_dir: str = "assets"):
self.assets_dir = assets_dir
self.font_dir = os.path.join(assets_dir, "fonts")
self.image_dir = os.path.join(assets_dir, "images")
# Colors - Premium Futuristic Dark Mode
self.bg_color = (10, 10, 11) # #0a0a0b
self.accent_color = (138, 43, 226) # BlueViolet
self.text_color = (255, 255, 255)
self.sub_text_color = (200, 200, 200)
self.muted_text_color = (150, 150, 150)
self.card_bg = (20, 20, 25, 180) # semi-transparent
# Initialize fonts (fallbacks)
self.fonts = {}
# System font paths to try on Mac/Linux
font_paths = [
os.path.join(self.font_dir, "Inter-Bold.ttf"),
os.path.join(self.font_dir, "Inter-Regular.ttf"),
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Supplemental/Arial.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
]
def load_font(size, bold=False):
for path in font_paths:
try:
# PIL can handle .ttc if you don't specify index (it takes index 0)
return ImageFont.truetype(path, size)
except Exception:
continue
return ImageFont.load_default()
self.fonts['title'] = load_font(72, True)
self.fonts['subtitle'] = load_font(36)
self.fonts['section'] = load_font(28, True)
self.fonts['body'] = load_font(22)
self.fonts['small'] = load_font(18)
def generate_dashboard(self, data: LabReportData) -> Image.Image:
# Create canvas (1920x1080 for high resolution)
width, height = 1920, 1080
img = Image.new('RGB', (width, height), color=self.bg_color)
draw = ImageDraw.Draw(img, 'RGBA')
# Add subtle radial gradient background or ambient glow
self._draw_ambient_glow(draw, width, height)
# 1. Header Section
self._draw_header(draw, data, width)
# 2. Left Panel: Cannabinoids
self._draw_cannabinoids(draw, data.cannabinoids)
# 3. Center: Visual (Flower/Placeholder)
self._draw_center_visual(img, draw)
# 4. Right Panel: Terpenes
self._draw_terpenes(draw, data.terpenes)
# 5. Bottom: Info Strip
self._draw_footer(draw, data, width, height)
# 6. Watermark
self._draw_watermark(draw, width, height)
return img
def _draw_watermark(self, draw, width, height):
text = "POWERED BY STRAINAI"
draw.text((width - 300, height - 60), text, font=self.fonts['small'], fill=(255, 255, 255, 80))
def _draw_ambient_glow(self, draw, width, height):
# Subtle purple glow in the center
center_x, center_y = width // 2, height // 2
for r in range(500, 0, -10):
alpha = int(30 * (1 - r/500))
draw.ellipse([center_x - r, center_y - r, center_x + r, center_y + r],
fill=(138, 43, 226, alpha))
def _draw_header(self, draw, data, width):
# Strain name
title = data.strain_name.upper()
subtitle = f"{data.strain_type or 'HYBRID'} | {data.dominance or 'BALANCED'}"
# Centered Header
title_bbox = draw.textbbox((0, 0), title, font=self.fonts['title'])
title_w = title_bbox[2] - title_bbox[0]
draw.text(((width - title_w) // 2, 80), title, font=self.fonts['title'], fill=self.text_color)
subtitle_bbox = draw.textbbox((0, 0), subtitle, font=self.fonts['subtitle'])
subtitle_w = subtitle_bbox[2] - subtitle_bbox[0]
draw.text(((width - subtitle_w) // 2, 160), subtitle, font=self.fonts['subtitle'], fill=self.accent_color)
def _draw_cannabinoids(self, draw, cannabinoids):
# Left panel card
x, y = 100, 250
w, h = 450, 600
self._draw_glass_card(draw, x, y, w, h, "CANNABINOID PROFILE")
# Draw bars
y_offset = y + 100
for i, cb in enumerate(cannabinoids[:8]): # Limit to 8
# Label
draw.text((x + 30, y_offset), f"{cb.name}", font=self.fonts['section'], fill=self.text_color)
draw.text((x + w - 100, y_offset), f"{cb.value}{cb.unit}", font=self.fonts['section'], fill=self.accent_color)
# Progress bar background
draw.rectangle([x + 30, y_offset + 40, x + w - 30, y_offset + 50], fill=(50, 50, 60, 255))
# Progress bar fill
bar_w = (cb.value / 35.0) * (w - 60) # Max 35% for normalization
bar_w = min(bar_w, w - 60)
draw.rectangle([x + 30, y_offset + 40, x + 30 + bar_w, y_offset + 50], fill=self.accent_color)
y_offset += 65
def _draw_terpenes(self, draw, terpenes):
# Right panel card
x, y = 1370, 250
w, h = 450, 600
self._draw_glass_card(draw, x, y, w, h, "TERPENE PROFILE")
# Rank terpenes
y_offset = y + 100
for i, terp in enumerate(terpenes[:8]):
draw.text((x + 30, y_offset), f"{i+1}. {terp.name}", font=self.fonts['body'], fill=self.text_color)
draw.text((x + w - 100, y_offset), f"{terp.value}{terp.unit}", font=self.fonts['body'], fill=self.accent_color)
y_offset += 55
def _draw_center_visual(self, img, draw):
# Placeholder or flower image
# For now, let's just use a stylized circle/glow
center_x, center_y = 1920 // 2, 1080 // 2
# Future: Load flower image, crop, add glow
pass
def _draw_footer(self, draw, data, width, height):
# Info strip at the bottom
y = height - 120
info_text = f"ORIGIN: {data.origin or 'N/A'} | LAB: {data.lab_name or 'N/A'} | BATCH: {data.batch or 'N/A'} | TEST DATE: {data.test_date or 'N/A'}"
bbox = draw.textbbox((0, 0), info_text, font=self.fonts['small'])
text_w = bbox[2] - bbox[0]
draw.text(((width - text_w) // 2, y), info_text, font=self.fonts['small'], fill=self.muted_text_color)
def _draw_glass_card(self, draw, x, y, w, h, title):
# Draw rounded rectangle with semi-transparent fill
draw.rounded_rectangle([x, y, x + w, y + h], radius=30, fill=self.card_bg, outline=(100, 100, 120, 100), width=2)
# Draw section title
draw.text((x + 30, y + 30), title, font=self.fonts['section'], fill=self.muted_text_color)
|