2026-06-09 10:41:13 +00:00
|
|
|
|
"""SVG packaging generator for Foodlinkk — rich design options."""
|
2026-06-09 00:41:27 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
|
MM_TO_PX = 3.7795275591
|
2026-06-09 00:41:27 +00:00
|
|
|
|
DEFAULT_BARCODE_VALUE = "8710000000012"
|
|
|
|
|
|
|
|
|
|
|
|
FOODLINKK_BRAND = {
|
|
|
|
|
|
"bg": "#0b1220",
|
|
|
|
|
|
"panel": "#101a2d",
|
|
|
|
|
|
"primary": "#00e5ff",
|
|
|
|
|
|
"secondary": "#ffd700",
|
2026-06-09 10:41:13 +00:00
|
|
|
|
"accent": "#b8ff3c",
|
2026-06-09 00:41:27 +00:00
|
|
|
|
"text": "#e2e8f0",
|
2026-06-09 10:41:13 +00:00
|
|
|
|
"muted": "#94a3b8",
|
2026-06-09 00:41:27 +00:00
|
|
|
|
"cut_line": "#ef4444",
|
|
|
|
|
|
"fold_line": "#60a5fa",
|
2026-06-09 10:41:13 +00:00
|
|
|
|
"bleed": "#f97316",
|
|
|
|
|
|
"halal": "#22c55e",
|
2026-06-09 00:41:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
|
PACKAGING_TYPES = (
|
|
|
|
|
|
"folding_box",
|
|
|
|
|
|
"wrap",
|
|
|
|
|
|
"round_label",
|
|
|
|
|
|
"sleeve",
|
|
|
|
|
|
"pouch",
|
|
|
|
|
|
"tray",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
|
|
|
|
|
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}"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
|
def _qr_placeholder_svg(size: int = 120) -> str:
|
|
|
|
|
|
"""Simple QR-style grid when qrcode lib unavailable."""
|
|
|
|
|
|
cell = max(size // 10, 8)
|
|
|
|
|
|
parts = [
|
|
|
|
|
|
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}" viewBox="0 0 {size} {size}">',
|
|
|
|
|
|
f'<rect width="{size}" height="{size}" fill="#fff"/>',
|
|
|
|
|
|
]
|
|
|
|
|
|
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'<rect x="{x}" y="{y}" width="{cell}" height="{cell}" fill="#111"/>')
|
|
|
|
|
|
parts.append("</svg>")
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
|
def generate_packaging(spec: dict[str, Any]) -> str:
|
2026-06-09 10:41:13 +00:00
|
|
|
|
"""Create an SVG packaging design from a rich spec."""
|
2026-06-09 00:41:27 +00:00
|
|
|
|
ptype = (spec.get("type") or "folding_box").strip().lower()
|
2026-06-09 10:41:13 +00:00
|
|
|
|
if ptype not in PACKAGING_TYPES:
|
|
|
|
|
|
raise ValueError(f"Unsupported packaging type: {ptype}")
|
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
|
width_mm = float(spec.get("width_mm", 120))
|
|
|
|
|
|
height_mm = float(spec.get("height_mm", 80))
|
|
|
|
|
|
depth_mm = float(spec.get("depth_mm", 40))
|
2026-06-09 10:41:13 +00:00
|
|
|
|
bleed_mm = float(spec.get("bleed_mm", 3))
|
2026-06-09 00:41:27 +00:00
|
|
|
|
elements = spec.get("elements", {})
|
|
|
|
|
|
brand = {**FOODLINKK_BRAND, **(spec.get("brand") or {})}
|
2026-06-09 10:41:13 +00:00
|
|
|
|
txt = _spec_text(spec)
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
|
canvas_w, canvas_h = _canvas_size(ptype, width_mm, height_mm, depth_mm)
|
2026-06-09 00:41:27 +00:00
|
|
|
|
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
|
2026-06-09 10:41:13 +00:00
|
|
|
|
body_w = body_h = x0 = y0 = 0.0
|
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
|
if ptype == "folding_box":
|
|
|
|
|
|
body_w = _mm(width_mm)
|
|
|
|
|
|
body_h = _mm(height_mm)
|
|
|
|
|
|
depth_w = _mm(depth_mm)
|
2026-06-09 10:41:13 +00:00
|
|
|
|
x0, y0 = margin, margin
|
2026-06-09 00:41:27 +00:00
|
|
|
|
panels = [depth_w, body_w, depth_w, body_w]
|
|
|
|
|
|
x = x0
|
|
|
|
|
|
for idx, panel_w in enumerate(panels):
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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))
|
2026-06-09 00:41:27 +00:00
|
|
|
|
x += panel_w
|
|
|
|
|
|
if _elements_enabled(elements, "fold_lines"):
|
|
|
|
|
|
x = x0 + panels[0]
|
|
|
|
|
|
for panel_w in panels[1:]:
|
2026-06-09 10:41:13 +00:00
|
|
|
|
dwg.add(dwg.line(start=(x, y0), end=(x, y0 + body_h), stroke=brand["fold_line"], stroke_dasharray="8,6", stroke_width=1.2))
|
2026-06-09 00:41:27 +00:00
|
|
|
|
x += panel_w
|
|
|
|
|
|
if _elements_enabled(elements, "cut_lines"):
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
|
|
|
|
|
elif ptype == "wrap":
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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":
|
2026-06-09 00:41:27 +00:00
|
|
|
|
body_w = _mm(width_mm)
|
|
|
|
|
|
body_h = _mm(height_mm)
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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
|
2026-06-09 00:41:27 +00:00
|
|
|
|
diameter = min(canvas_w, canvas_h) - (margin * 2)
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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))
|
2026-06-09 00:41:27 +00:00
|
|
|
|
if _elements_enabled(elements, "cut_lines"):
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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)
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
|
|
|
|
|
if _elements_enabled(elements, "logo_area"):
|
2026-06-09 10:41:13 +00:00
|
|
|
|
_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)
|
2026-06-09 00:41:27 +00:00
|
|
|
|
|
|
|
|
|
|
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)))
|
|
|
|
|
|
|
2026-06-09 10:41:13 +00:00
|
|
|
|
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",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-09 00:41:27 +00:00
|
|
|
|
return dwg.tostring()
|