234 lines
7.0 KiB
Python
234 lines
7.0 KiB
Python
|
|
"""SVG packaging generator for Foodlinkk."""
|
||
|
|
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 # 96 DPI conversion
|
||
|
|
DEFAULT_BARCODE_VALUE = "8710000000012"
|
||
|
|
|
||
|
|
FOODLINKK_BRAND = {
|
||
|
|
"bg": "#0b1220",
|
||
|
|
"panel": "#101a2d",
|
||
|
|
"primary": "#00e5ff",
|
||
|
|
"secondary": "#ffd700",
|
||
|
|
"text": "#e2e8f0",
|
||
|
|
"cut_line": "#ef4444",
|
||
|
|
"fold_line": "#60a5fa",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
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 generate_packaging(spec: dict[str, Any]) -> str:
|
||
|
|
"""Create an SVG packaging design based on a simple spec."""
|
||
|
|
ptype = (spec.get("type") or "folding_box").strip().lower()
|
||
|
|
width_mm = float(spec.get("width_mm", 120))
|
||
|
|
height_mm = float(spec.get("height_mm", 80))
|
||
|
|
depth_mm = float(spec.get("depth_mm", 40))
|
||
|
|
elements = spec.get("elements", {})
|
||
|
|
brand = {**FOODLINKK_BRAND, **(spec.get("brand") or {})}
|
||
|
|
|
||
|
|
if ptype == "folding_box":
|
||
|
|
canvas_w = _mm((width_mm * 2) + (depth_mm * 2) + 20)
|
||
|
|
canvas_h = _mm(height_mm + depth_mm + 20)
|
||
|
|
elif ptype == "wrap":
|
||
|
|
canvas_w = _mm(width_mm + 20)
|
||
|
|
canvas_h = _mm(height_mm + 20)
|
||
|
|
elif ptype == "round_label":
|
||
|
|
diameter = max(min(width_mm, height_mm), 20)
|
||
|
|
canvas_w = _mm(diameter + 20)
|
||
|
|
canvas_h = _mm(diameter + 20)
|
||
|
|
else:
|
||
|
|
raise ValueError(f"Unsupported packaging type: {ptype}")
|
||
|
|
|
||
|
|
dwg = svgwrite.Drawing(size=(canvas_w, canvas_h))
|
||
|
|
dwg.viewbox(0, 0, canvas_w, canvas_h)
|
||
|
|
|
||
|
|
# Background and frame
|
||
|
|
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
|
||
|
|
if ptype == "folding_box":
|
||
|
|
body_w = _mm(width_mm)
|
||
|
|
body_h = _mm(height_mm)
|
||
|
|
depth_w = _mm(depth_mm)
|
||
|
|
x0 = margin
|
||
|
|
y0 = margin
|
||
|
|
panels = [depth_w, body_w, depth_w, body_w]
|
||
|
|
x = x0
|
||
|
|
for idx, panel_w in enumerate(panels):
|
||
|
|
dwg.add(
|
||
|
|
dwg.rect(
|
||
|
|
insert=(x, y0),
|
||
|
|
size=(panel_w, body_h),
|
||
|
|
fill="none",
|
||
|
|
stroke=brand["primary"] if idx % 2 else brand["secondary"],
|
||
|
|
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,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
logo_x = x0 + panels[0] + (_mm(width_mm) * 0.12)
|
||
|
|
logo_y = y0 + (_mm(height_mm) * 0.16)
|
||
|
|
logo_w = _mm(width_mm) * 0.76
|
||
|
|
logo_h = _mm(height_mm) * 0.42
|
||
|
|
elif ptype == "wrap":
|
||
|
|
body_w = _mm(width_mm)
|
||
|
|
body_h = _mm(height_mm)
|
||
|
|
x0 = margin
|
||
|
|
y0 = 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, "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 = x0 + (body_w * 0.14)
|
||
|
|
logo_y = y0 + (body_h * 0.14)
|
||
|
|
logo_w = body_w * 0.72
|
||
|
|
logo_h = body_h * 0.36
|
||
|
|
else:
|
||
|
|
diameter = min(canvas_w, canvas_h) - (margin * 2)
|
||
|
|
cx = canvas_w / 2
|
||
|
|
cy = 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 = diameter * 0.64
|
||
|
|
logo_h = diameter * 0.22
|
||
|
|
logo_x = cx - (logo_w / 2)
|
||
|
|
logo_y = cy - (logo_h / 2) - 8
|
||
|
|
body_w = diameter
|
||
|
|
body_h = diameter
|
||
|
|
x0 = cx - (diameter / 2)
|
||
|
|
y0 = cy - (diameter / 2)
|
||
|
|
|
||
|
|
if _elements_enabled(elements, "logo_area"):
|
||
|
|
dwg.add(
|
||
|
|
dwg.rect(
|
||
|
|
insert=(logo_x, logo_y),
|
||
|
|
size=(logo_w, logo_h),
|
||
|
|
rx=8,
|
||
|
|
ry=8,
|
||
|
|
fill="none",
|
||
|
|
stroke=brand["secondary"],
|
||
|
|
stroke_width=2,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
dwg.add(
|
||
|
|
dwg.text(
|
||
|
|
"FOODLINKK",
|
||
|
|
insert=(logo_x + 12, logo_y + (logo_h / 2) + 5),
|
||
|
|
fill=brand["text"],
|
||
|
|
font_size=18,
|
||
|
|
font_family="Arial, sans-serif",
|
||
|
|
font_weight="bold",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
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)))
|
||
|
|
|
||
|
|
return dwg.tostring()
|