"""SVG packaging generator for Foodlinkk — rich design options.""" from __future__ import annotations import base64 from io import BytesIO from typing import Any import svgwrite from barcode import Code128 from barcode.writer import SVGWriter MM_TO_PX = 3.7795275591 DEFAULT_BARCODE_VALUE = "8710000000012" FOODLINKK_BRAND = { "bg": "#0b1220", "panel": "#101a2d", "primary": "#00e5ff", "secondary": "#ffd700", "accent": "#b8ff3c", "text": "#e2e8f0", "muted": "#94a3b8", "cut_line": "#ef4444", "fold_line": "#60a5fa", "bleed": "#f97316", "halal": "#22c55e", } PACKAGING_TYPES = ( "folding_box", "wrap", "round_label", "sleeve", "pouch", "tray", ) def _mm(mm: float) -> float: return round(float(mm) * MM_TO_PX, 2) def _elements_enabled(elements: Any, name: str) -> bool: if isinstance(elements, dict): return bool(elements.get(name)) if isinstance(elements, list): return name in elements return False def _barcode_data_uri(value: str) -> str: barcode = Code128(value, writer=SVGWriter()) stream = BytesIO() barcode.write(stream) encoded = base64.b64encode(stream.getvalue()).decode("ascii") return f"data:image/svg+xml;base64,{encoded}" def _qr_placeholder_svg(size: int = 120) -> str: """Simple QR-style grid when qrcode lib unavailable.""" cell = max(size // 10, 8) parts = [ f'', f'', ] for y in range(0, size, cell): for x in range(0, size, cell): if (x + y) % (cell * 2) == 0 or x < cell * 3 and y < cell * 3 or x > size - cell * 4 and y < cell * 3: parts.append(f'') parts.append("") raw = "".join(parts).encode("utf-8") return f"data:image/svg+xml;base64,{base64.b64encode(raw).decode('ascii')}" def _spec_text(spec: dict[str, Any]) -> dict[str, str]: text = spec.get("text") or {} if not isinstance(text, dict): text = {} brand = {**FOODLINKK_BRAND, **(spec.get("brand") or {})} return { "product_name": str(text.get("product_name") or brand.get("product_name") or "FOODLINKK"), "tagline": str(text.get("tagline") or brand.get("tagline") or "Premium food solutions"), "subtitle": str(text.get("subtitle") or text.get("weight") or ""), "ingredients": str(text.get("ingredients") or ""), "best_before": str(text.get("best_before") or "Ten minste houdbaar tot: zie verpakking"), "origin": str(text.get("origin") or "Geproduceerd in NL"), } def _nutrition_rows(spec: dict[str, Any]) -> list[tuple[str, str]]: text = spec.get("text") or {} rows = text.get("nutrition") if isinstance(text, dict) else None if isinstance(rows, list) and rows: out: list[tuple[str, str]] = [] for row in rows[:12]: if isinstance(row, dict): out.append((str(row.get("k") or row.get("label") or ""), str(row.get("v") or row.get("value") or ""))) return out return [ ("Energie", "450 kJ / 107 kcal"), ("Vetten", "4.2 g"), ("waarvan verzadigd", "1.1 g"), ("Koolhydraten", "12 g"), ("waarvan suikers", "2.8 g"), ("Eiwitten", "6.5 g"), ("Zout", "0.85 g"), ] def _draw_bleed(dwg: svgwrite.Drawing, x: float, y: float, w: float, h: float, brand: dict[str, str], bleed_mm: float) -> None: if bleed_mm <= 0: return b = _mm(bleed_mm) dwg.add( dwg.rect( insert=(x - b, y - b), size=(w + 2 * b, h + 2 * b), fill="none", stroke=brand["bleed"], stroke_dasharray="6,4", stroke_width=1.0, stroke_opacity=0.7, ) ) def _draw_logo_block(dwg: svgwrite.Drawing, x: float, y: float, w: float, h: float, brand: dict[str, str], txt: dict[str, str]) -> None: dwg.add( dwg.rect(insert=(x, y), size=(w, h), rx=10, ry=10, fill=brand["panel"], stroke=brand["secondary"], stroke_width=2) ) dwg.add( dwg.text( txt["product_name"][:42], insert=(x + 14, y + h * 0.42), fill=brand["text"], font_size=min(22, max(12, w * 0.045)), font_family="Arial, Helvetica, sans-serif", font_weight="bold", ) ) if txt["tagline"]: dwg.add( dwg.text( txt["tagline"][:60], insert=(x + 14, y + h * 0.62), fill=brand["primary"], font_size=min(14, max(9, w * 0.028)), font_family="Arial, Helvetica, sans-serif", ) ) if txt["subtitle"]: dwg.add( dwg.text( txt["subtitle"][:24], insert=(x + 14, y + h * 0.82), fill=brand["muted"], font_size=11, font_family="Arial, Helvetica, sans-serif", ) ) def _draw_nutrition_panel( dwg: svgwrite.Drawing, x: float, y: float, w: float, h: float, brand: dict[str, str], rows: list[tuple[str, str]] ) -> None: dwg.add(dwg.rect(insert=(x, y), size=(w, h), fill="#ffffff", stroke=brand["text"], stroke_width=1.2, rx=4)) dwg.add( dwg.text( "Voedingswaarden per 100g", insert=(x + 8, y + 16), fill="#111827", font_size=11, font_weight="bold", font_family="Arial, sans-serif", ) ) line_y = y + 26 for label, value in rows: dwg.add(dwg.text(label[:28], insert=(x + 8, line_y), fill="#374151", font_size=9, font_family="Arial, sans-serif")) dwg.add(dwg.text(value[:16], insert=(x + w - 8, line_y), fill="#111827", font_size=9, font_family="Arial, sans-serif", text_anchor="end")) line_y += 13 if line_y > y + h - 6: break def _draw_ingredients(dwg: svgwrite.Drawing, x: float, y: float, w: float, text: str, brand: dict[str, str]) -> None: dwg.add( dwg.text( "Ingrediënten:", insert=(x, y), fill=brand["text"], font_size=10, font_weight="bold", font_family="Arial, sans-serif", ) ) chunk = text[:220] or "Ingrediënten volgens recept — vul aan in Packaging Studio." words, line, line_y, line_h = chunk.split(), "", y + 14, 12 for word in words: test = (line + " " + word).strip() if len(test) > 42: dwg.add(dwg.text(line, insert=(x, line_y), fill=brand["muted"], font_size=9, font_family="Arial, sans-serif")) line, line_y = word, line_y + line_h else: line = test if line: dwg.add(dwg.text(line, insert=(x, line_y), fill=brand["muted"], font_size=9, font_family="Arial, sans-serif")) def _draw_halal_badge(dwg: svgwrite.Drawing, x: float, y: float, brand: dict[str, str]) -> None: r = 28 dwg.add(dwg.circle(center=(x + r, y + r), r=r, fill=brand["halal"], stroke="#ffffff", stroke_width=2)) dwg.add( dwg.text( "HALAL", insert=(x + r, y + r + 5), fill="#052e16", font_size=11, font_weight="bold", font_family="Arial, sans-serif", text_anchor="middle", ) ) def _draw_window(dwg: svgwrite.Drawing, x: float, y: float, w: float, h: float) -> None: dwg.add(dwg.rect(insert=(x, y), size=(w, h), fill="#bae6fd", fill_opacity=0.35, stroke="#38bdf8", stroke_width=1.5, rx=6)) dwg.add(dwg.text("WINDOW", insert=(x + w / 2, y + h / 2 + 4), fill="#0c4a6e", font_size=10, text_anchor="middle", font_family="Arial, sans-serif")) def _draw_dimensions(dwg: svgwrite.Drawing, x: float, y: float, w: float, h: float, spec: dict[str, Any], brand: dict[str, str]) -> None: label = f"{spec.get('width_mm')} × {spec.get('height_mm')} × {spec.get('depth_mm')} mm" dwg.add( dwg.text( label, insert=(x + w / 2, y + h + 18), fill=brand["muted"], font_size=10, text_anchor="middle", font_family="Arial, sans-serif", ) ) def _canvas_size(ptype: str, width_mm: float, height_mm: float, depth_mm: float) -> tuple[float, float]: if ptype == "folding_box": return _mm((width_mm * 2) + (depth_mm * 2) + 20), _mm(height_mm + depth_mm + 20) if ptype == "wrap": return _mm(width_mm + 20), _mm(height_mm + 20) if ptype == "round_label": d = max(min(width_mm, height_mm), 20) return _mm(d + 20), _mm(d + 20) if ptype == "sleeve": return _mm((width_mm * 2) + depth_mm + 20), _mm(height_mm + 20) if ptype == "pouch": return _mm(width_mm + 20), _mm(height_mm + depth_mm + 20) if ptype == "tray": return _mm(width_mm + 20), _mm(height_mm + depth_mm + 20) raise ValueError(f"Unsupported packaging type: {ptype}") def generate_packaging(spec: dict[str, Any]) -> str: """Create an SVG packaging design from a rich spec.""" ptype = (spec.get("type") or "folding_box").strip().lower() if ptype not in PACKAGING_TYPES: raise ValueError(f"Unsupported packaging type: {ptype}") width_mm = float(spec.get("width_mm", 120)) height_mm = float(spec.get("height_mm", 80)) depth_mm = float(spec.get("depth_mm", 40)) bleed_mm = float(spec.get("bleed_mm", 3)) elements = spec.get("elements", {}) brand = {**FOODLINKK_BRAND, **(spec.get("brand") or {})} txt = _spec_text(spec) canvas_w, canvas_h = _canvas_size(ptype, width_mm, height_mm, depth_mm) dwg = svgwrite.Drawing(size=(canvas_w, canvas_h)) dwg.viewbox(0, 0, canvas_w, canvas_h) dwg.add(dwg.rect(insert=(0, 0), size=(canvas_w, canvas_h), fill=brand["bg"])) dwg.add( dwg.rect( insert=(4, 4), size=(canvas_w - 8, canvas_h - 8), fill=brand["panel"], rx=10, ry=10, stroke=brand["primary"], stroke_opacity=0.25, stroke_width=2, ) ) margin = 24 body_w = body_h = x0 = y0 = 0.0 if ptype == "folding_box": body_w = _mm(width_mm) body_h = _mm(height_mm) depth_w = _mm(depth_mm) x0, y0 = margin, margin panels = [depth_w, body_w, depth_w, body_w] x = x0 for idx, panel_w in enumerate(panels): fill = brand["primary"] if idx % 2 else brand["secondary"] dwg.add(dwg.rect(insert=(x, y0), size=(panel_w, body_h), fill="none", stroke=fill, stroke_opacity=0.45, stroke_width=1.6)) x += panel_w if _elements_enabled(elements, "fold_lines"): x = x0 + panels[0] for panel_w in panels[1:]: dwg.add(dwg.line(start=(x, y0), end=(x, y0 + body_h), stroke=brand["fold_line"], stroke_dasharray="8,6", stroke_width=1.2)) x += panel_w if _elements_enabled(elements, "cut_lines"): dwg.add(dwg.rect(insert=(x0, y0), size=(sum(panels), body_h), fill="none", stroke=brand["cut_line"], stroke_dasharray="5,4", stroke_width=1.1)) if _elements_enabled(elements, "glue_tabs"): tab_w, tab_h = _mm(12), _mm(8) dwg.add(dwg.rect(insert=(x0 - tab_w, y0 + body_h * 0.4), size=(tab_w, tab_h), fill=brand["accent"], fill_opacity=0.35, stroke=brand["accent"])) logo_x = x0 + panels[0] + (_mm(width_mm) * 0.1) logo_y = y0 + (_mm(height_mm) * 0.12) logo_w = _mm(width_mm) * 0.8 logo_h = _mm(height_mm) * 0.38 elif ptype == "wrap": body_w, body_h = _mm(width_mm), _mm(height_mm) x0, y0 = margin, margin dwg.add(dwg.rect(insert=(x0, y0), size=(body_w, body_h), fill="none", stroke=brand["primary"], stroke_width=2.2)) if _elements_enabled(elements, "fold_lines"): dwg.add(dwg.line(start=(x0 + body_w / 2, y0), end=(x0 + body_w / 2, y0 + body_h), stroke=brand["fold_line"], stroke_dasharray="8,6", stroke_width=1.2)) if _elements_enabled(elements, "cut_lines"): dwg.add(dwg.rect(insert=(x0, y0), size=(body_w, body_h), fill="none", stroke=brand["cut_line"], stroke_dasharray="6,4", stroke_width=1.1)) logo_x, logo_y = x0 + body_w * 0.1, y0 + body_h * 0.12 logo_w, logo_h = body_w * 0.8, body_h * 0.35 elif ptype == "sleeve": body_w, body_h = _mm(width_mm), _mm(height_mm) depth_w = _mm(depth_mm) x0, y0 = margin, margin panels = [body_w, depth_w, body_w] x = x0 for panel_w in panels: dwg.add(dwg.rect(insert=(x, y0), size=(panel_w, body_h), fill="none", stroke=brand["primary"], stroke_width=1.8)) x += panel_w if _elements_enabled(elements, "fold_lines"): x = x0 + panels[0] for panel_w in panels[1:]: dwg.add(dwg.line(start=(x, y0), end=(x, y0 + body_h), stroke=brand["fold_line"], stroke_dasharray="8,6", stroke_width=1.2)) x += panel_w logo_x, logo_y = x0 + body_w * 0.12, y0 + body_h * 0.15 logo_w, logo_h = body_w * 0.76, body_h * 0.4 elif ptype == "pouch": body_w = _mm(width_mm) body_h = _mm(height_mm) seal_h = _mm(max(depth_mm, 15)) x0, y0 = margin, margin + seal_h dwg.add(dwg.rect(insert=(x0, y0), size=(body_w, body_h), fill="none", stroke=brand["primary"], stroke_width=2)) dwg.add(dwg.rect(insert=(x0, y0 - seal_h), size=(body_w, seal_h), fill=brand["secondary"], fill_opacity=0.2, stroke=brand["secondary"])) dwg.add(dwg.text("SEAL", insert=(x0 + body_w / 2, y0 - seal_h / 2 + 4), fill=brand["text"], font_size=10, text_anchor="middle", font_family="Arial, sans-serif")) logo_x, logo_y = x0 + body_w * 0.12, y0 + body_h * 0.18 logo_w, logo_h = body_w * 0.76, body_h * 0.42 elif ptype == "tray": body_w, body_h = _mm(width_mm), _mm(height_mm) lip = _mm(max(depth_mm, 8)) x0, y0 = margin, margin dwg.add(dwg.rect(insert=(x0, y0), size=(body_w, body_h), fill="none", stroke=brand["primary"], stroke_width=2.4)) dwg.add(dwg.rect(insert=(x0 + lip, y0 + lip), size=(body_w - 2 * lip, body_h - 2 * lip), fill="none", stroke=brand["fold_line"], stroke_dasharray="5,4", stroke_width=1)) logo_x, logo_y = x0 + lip + 8, y0 + lip + 8 logo_w, logo_h = body_w - 2 * lip - 16, (body_h - 2 * lip) * 0.45 else: # round_label diameter = min(canvas_w, canvas_h) - (margin * 2) cx, cy = canvas_w / 2, canvas_h / 2 dwg.add(dwg.circle(center=(cx, cy), r=diameter / 2, fill="none", stroke=brand["primary"], stroke_width=2.4)) if _elements_enabled(elements, "cut_lines"): dwg.add(dwg.circle(center=(cx, cy), r=(diameter / 2) - 4, fill="none", stroke=brand["cut_line"], stroke_dasharray="4,4", stroke_width=1.0)) logo_w, logo_h = diameter * 0.64, diameter * 0.22 logo_x, logo_y = cx - (logo_w / 2), cy - (logo_h / 2) - 8 body_w = body_h = diameter x0, y0 = cx - (diameter / 2), cy - (diameter / 2) if _elements_enabled(elements, "bleed"): _draw_bleed(dwg, x0, y0, body_w if ptype != "folding_box" else sum([_mm(depth_mm), _mm(width_mm), _mm(depth_mm), _mm(width_mm)]), body_h, brand, bleed_mm) if _elements_enabled(elements, "logo_area"): _draw_logo_block(dwg, logo_x, logo_y, logo_w, logo_h, brand, txt) if _elements_enabled(elements, "window"): wx = x0 + body_w * 0.55 if ptype != "round_label" else logo_x + logo_w * 0.1 wy = y0 + body_h * 0.55 if ptype != "round_label" else logo_y + logo_h + 8 _draw_window(dwg, wx, wy, max(60, body_w * 0.32), max(40, body_h * 0.22)) if _elements_enabled(elements, "halal_badge"): _draw_halal_badge(dwg, x0 + 8, y0 + 8, brand) if _elements_enabled(elements, "nutrition_panel"): nx = x0 + 8 ny = y0 + body_h - min(120, body_h * 0.45) _draw_nutrition_panel(dwg, nx, ny, min(150, body_w * 0.42), min(110, body_h * 0.4), brand, _nutrition_rows(spec)) if _elements_enabled(elements, "ingredients") and txt["ingredients"]: _draw_ingredients(dwg, x0 + 8, y0 + body_h - 28, body_w - 16, txt["ingredients"], brand) if _elements_enabled(elements, "barcode"): barcode_uri = _barcode_data_uri(str(spec.get("barcode_value") or DEFAULT_BARCODE_VALUE)) bar_w = max(140, body_w * 0.35) bar_h = max(50, body_h * 0.16) bar_x = x0 + body_w - bar_w - 14 bar_y = y0 + body_h - bar_h - 14 dwg.add(dwg.rect(insert=(bar_x - 4, bar_y - 4), size=(bar_w + 8, bar_h + 8), fill="#ffffff")) dwg.add(dwg.image(href=barcode_uri, insert=(bar_x, bar_y), size=(bar_w, bar_h))) if _elements_enabled(elements, "qr_code"): qr_uri = _qr_placeholder_svg(100) qr_s = min(90, body_w * 0.22) dwg.add(dwg.image(href=qr_uri, insert=(x0 + 10, y0 + body_h - qr_s - 10), size=(qr_s, qr_s))) if _elements_enabled(elements, "dimensions"): _draw_dimensions(dwg, x0, y0, body_w, body_h, spec, brand) design_name = spec.get("design_name") or txt["product_name"] dwg.add( dwg.text( f"Foodlinkk Packaging Studio · {design_name}"[:80], insert=(12, canvas_h - 10), fill=brand["muted"], font_size=9, font_family="Arial, sans-serif", ) ) return dwg.tostring()