StrainAIAPP / services /renderer.py
Victor Gerardo Rivera
Initial commit
fd027e9
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)