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)