From 6ba7038067b2d553330ede6f7ba541386b0fd830 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 20:42:18 +0000 Subject: [PATCH 1/3] chore: reorder syrup flavor registry to required lineup Set export order to Original, Mango, Strawberry, Grape. No flavors added or removed. Co-authored-by: ebyron357 --- README.md | 2 +- config/syrup/flavors.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index df6fbc6..add5d63 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Output: `output/production_v2/` ## Syrup Labels (Master Compliance + Production Rebuild) -4 PDFs — Original · Grape · Strawberry · Mango +4 PDFs — Original · Mango · Strawberry · Grape ```bash python3 scripts/export_syrup_production.py diff --git a/config/syrup/flavors.yaml b/config/syrup/flavors.yaml index 6718d38..71456da 100644 --- a/config/syrup/flavors.yaml +++ b/config/syrup/flavors.yaml @@ -6,17 +6,17 @@ flavors: display_name: "Original" accent_color: champagne_gold - - id: grape - name: "GRAPE" - display_name: "Grape" - accent_color: berry_accent + - id: mango + name: "MANGO" + display_name: "Mango" + accent_color: citrus_accent - id: strawberry name: "STRAWBERRY" display_name: "Strawberry" accent_color: berry_accent - - id: mango - name: "MANGO" - display_name: "Mango" - accent_color: citrus_accent + - id: grape + name: "GRAPE" + display_name: "Grape" + accent_color: berry_accent From f88df888bceda29d0f4c7a8d44ae00001bb135f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 21:04:00 +0000 Subject: [PATCH 2/3] fix: Tier 1 syrup label production readiness improvements - Resolve back-panel zone collisions with non-overlapping layout bands - Raise panel_body to 6.0 and supplement_body to 5.5 for legibility - Enlarge QR zone and separate website text below QR block - Render ingredients as wrapped paragraph from compliance string - Add discreet 21+ age gate on front panel Locked product facts, brand, flavors, and QR URL unchanged. Co-authored-by: ebyron357 --- config/syrup/brand.yaml | 5 +- src/alt_syrup/layout.py | 45 ++++-------- src/alt_syrup/panels/back_panel.py | 92 +++++++++++++++++------- src/alt_syrup/panels/front_panel.py | 7 +- src/alt_syrup/panels/supplement_facts.py | 4 +- 5 files changed, 90 insertions(+), 63 deletions(-) diff --git a/config/syrup/brand.yaml b/config/syrup/brand.yaml index 54ade46..c828f97 100644 --- a/config/syrup/brand.yaml +++ b/config/syrup/brand.yaml @@ -7,6 +7,7 @@ product_line: syrup brand: name: "ALTERNATIVE™" website: "AlternativeBev.com" + age_gate: "21+" statement_of_identity: "Hemp-Derived Delta-9 THC Syrup" canvas: @@ -40,9 +41,9 @@ typography: servings: 7.5 net_contents: 7.0 panel_heading: 6.5 - panel_body: 5.5 + panel_body: 6.0 supplement_heading: 6.0 - supplement_body: 5.0 + supplement_body: 5.5 product: total_thc_mg: 420 diff --git a/src/alt_syrup/layout.py b/src/alt_syrup/layout.py index 24b91ad..3a9d91f 100644 --- a/src/alt_syrup/layout.py +++ b/src/alt_syrup/layout.py @@ -58,38 +58,19 @@ def build_layout() -> SyrupLayout: back.width - 2 * safe_inset, back.height - 2 * safe_inset, ) - supplement_zone = Rect( - safe_back.x, safe_back.y + safe_back.height * 0.42, - safe_back.width * 0.48, safe_back.height * 0.38, - ) - directions_zone = Rect( - safe_back.x, safe_back.y + safe_back.height * 0.72, - safe_back.width, safe_back.height * 0.26, - ) - ingredients_zone = Rect( - safe_back.x + safe_back.width * 0.50, safe_back.y + safe_back.height * 0.42, - safe_back.width * 0.50, safe_back.height * 0.28, - ) - warning_zone = Rect( - safe_back.x, safe_back.y + safe_back.height * 0.08, - safe_back.width, safe_back.height * 0.32, - ) - responsible_zone = Rect( - safe_back.x, safe_back.y + safe_back.height * 0.80, - safe_back.width * 0.55, safe_back.height * 0.18, - ) - qr_zone = Rect( - safe_back.x, safe_back.y, - safe_back.width * 0.42, safe_back.height * 0.10, - ) - barcode_zone = Rect( - safe_back.x + safe_back.width * 0.55, safe_back.y, - safe_back.width * 0.45, safe_back.height * 0.10, - ) - lot_zone = Rect( - safe_back.x, safe_back.y + safe_back.height * 0.94, - safe_back.width, safe_back.height * 0.06, - ) + # Non-overlapping vertical bands (percentages of safe_back.height) + h = safe_back.height + x0 = safe_back.x + w = safe_back.width + + qr_zone = Rect(x0, safe_back.y, w * 0.58, h * 0.15) + barcode_zone = Rect(x0 + w * 0.58, safe_back.y, w * 0.42, h * 0.15) + warning_zone = Rect(x0, safe_back.y + h * 0.15, w, h * 0.23) + supplement_zone = Rect(x0, safe_back.y + h * 0.38, w * 0.48, h * 0.24) + ingredients_zone = Rect(x0 + w * 0.50, safe_back.y + h * 0.38, w * 0.50, h * 0.24) + directions_zone = Rect(x0, safe_back.y + h * 0.62, w, h * 0.15) + responsible_zone = Rect(x0, safe_back.y + h * 0.77, w, h * 0.14) + lot_zone = Rect(x0, safe_back.y + h * 0.91, w, h * 0.09) return SyrupLayout( width=full_w, diff --git a/src/alt_syrup/panels/back_panel.py b/src/alt_syrup/panels/back_panel.py index f5cb49b..f2502b5 100644 --- a/src/alt_syrup/panels/back_panel.py +++ b/src/alt_syrup/panels/back_panel.py @@ -11,6 +11,14 @@ from .supplement_facts import render_supplement_facts +def _body_size(typo: dict) -> float: + return max(typo.get("panel_body", 6.0), 6.0) + + +def _lot_size(typo: dict) -> float: + return max(typo.get("panel_body", 6.0) - 0.5, 5.5) + + def render_back_panel( c: Canvas, layout: SyrupLayout, @@ -34,8 +42,15 @@ def render_back_panel( def _render_qr(c: Canvas, layout: SyrupLayout, brand: dict, compliance: dict, typo: dict) -> None: zone = layout.qr_zone - qr_size = min(zone.height * 0.85, zone.width * 0.55) - quiet = qr_size * brand["qr_section"].get("quiet_zone_ratio", 0.12) + body = _body_size(typo) + website_band = body * 1.6 + content_h = zone.height - website_band + quiet_ratio = brand["qr_section"].get("quiet_zone_ratio", 0.12) + qr_size = min(content_h * 0.82, zone.width * 0.44) + quiet = qr_size * quiet_ratio + qr_x = zone.x + quiet + qr_y = zone.y + website_band + max((content_h - qr_size) / 2, quiet) + url = compliance.get("qr_url", f"https://{brand['brand']['website']}") qr = qrcode.QRCode(version=1, box_size=10, border=4) qr.add_data(url) @@ -44,16 +59,18 @@ def _render_qr(c: Canvas, layout: SyrupLayout, brand: dict, compliance: dict, ty buf = io.BytesIO() img.save(buf, format="PNG") buf.seek(0) - c.drawImage(ImageReader(buf), zone.x + quiet, zone.y + quiet, qr_size, qr_size, mask="auto") - tx = zone.x + quiet + qr_size + quiet - ty = zone.y + zone.height - typo["panel_heading"] + c.drawImage(ImageReader(buf), qr_x, qr_y, qr_size, qr_size, mask="auto") + + tx = qr_x + qr_size + quiet + ty = zone.y + zone.height - website_band - 2 c.setFillColor(WARM_OFF_WHITE) - c.setFont("Helvetica-Bold", typo["panel_heading"] - 1) + c.setFont("Helvetica-Bold", typo["panel_heading"] - 0.5) for line in brand["qr_section"]["heading_lines"]: c.drawString(tx, ty, line) - ty -= typo["panel_heading"] - c.setFont("Helvetica", typo["panel_body"] - 0.5) - c.drawString(zone.x, zone.y, brand["brand"]["website"]) + ty -= typo["panel_heading"] * 0.95 + + c.setFont("Helvetica", body) + c.drawString(zone.x, zone.y + 1, brand["brand"]["website"]) def _render_barcode(c: Canvas, layout: SyrupLayout, compliance: dict) -> None: @@ -71,7 +88,7 @@ def _render_barcode(c: Canvas, layout: SyrupLayout, compliance: dict) -> None: c.setFillColor(WARM_OFF_WHITE) c.rect(x, bar_y, bar_w, bar_h, fill=1, stroke=0) x += bar_w - c.setFont("Helvetica", 5) + c.setFont("Helvetica", 5.5) c.drawCentredString(zone.x + zone.width / 2, zone.y + 1, upc) @@ -88,66 +105,74 @@ def _upc_pattern(digits: str) -> str: def _render_directions(c: Canvas, layout: SyrupLayout, brand: dict, typo: dict) -> None: zone = layout.directions_zone + body = _body_size(typo) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, brand["directions"]["heading"]) - y -= typo["panel_heading"] * 1.2 - c.setFont("Helvetica", typo["panel_body"] - 0.5) + y -= typo["panel_heading"] * 1.15 + c.setFont("Helvetica", body) for line in brand["directions"]["lines"]: c.drawString(zone.x, y, line) - y -= typo["panel_body"] * 1.1 + y -= body * 1.05 def _render_ingredients(c: Canvas, layout: SyrupLayout, compliance: dict, typo: dict) -> None: zone = layout.ingredients_zone + body = _body_size(typo) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, "INGREDIENTS:") - y -= typo["panel_heading"] * 1.1 - c.setFont("Helvetica", typo["panel_body"] - 0.5) - for line in compliance.get("ingredients_lines", []): + y -= typo["panel_heading"] * 1.05 + c.setFont("Helvetica", body) + text = compliance.get("ingredients", "") + if not text: + text = ", ".join(compliance.get("ingredients_lines", [])) + for line in _wrap_to_width(c, text, "Helvetica", body, zone.width - 2): c.drawString(zone.x, y, line) - y -= typo["panel_body"] * 1.05 + y -= body * 1.08 def _render_warnings(c: Canvas, layout: SyrupLayout, brand: dict, compliance: dict, typo: dict) -> None: zone = layout.warning_zone + body = _body_size(typo) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, brand["warning_panel"]["heading"]) - y -= typo["panel_heading"] * 1.2 - c.setFont("Helvetica", typo["panel_body"] - 0.6) + y -= typo["panel_heading"] * 1.1 + c.setFont("Helvetica", body) for line in brand["warning_panel"]["lines"]: c.drawString(zone.x, y, line) - y -= typo["panel_body"] * 1.05 + y -= body * 1.02 for sw in compliance.get("state_warnings", [])[:2]: - for part in _wrap(sw, 38): + for part in _wrap(sw, 36): c.drawString(zone.x, y, part) - y -= typo["panel_body"] + y -= body * 1.02 def _render_responsible_party(c: Canvas, layout: SyrupLayout, brand: dict, typo: dict) -> None: zone = layout.responsible_zone + body = _body_size(typo) rp = brand["responsible_party"] - y = zone.y + zone.height - typo["panel_body"] + y = zone.y + zone.height - body c.setFillColor(WARM_OFF_WHITE) - c.setFont("Helvetica", typo["panel_body"] - 0.6) + c.setFont("Helvetica", body) for line in [ rp["manufactured_by_label"], rp["manufactured_by"], rp["manufactured_for_label"], rp["manufactured_for"], *rp["address_lines"], ]: c.drawString(zone.x, y, line) - y -= typo["panel_body"] * 0.95 + y -= body * 0.92 def _render_lot(c: Canvas, layout: SyrupLayout, compliance: dict, typo: dict) -> None: zone = layout.lot_zone + size = _lot_size(typo) c.setFillColor(WARM_OFF_WHITE) - c.setFont("Helvetica", typo["panel_body"] - 1) + c.setFont("Helvetica", size) lot = compliance.get("lot_number", "") best = compliance.get("best_by", "") c.drawString(zone.x, zone.y + 2, f"Lot: {lot}" if lot else "Lot:") @@ -167,3 +192,18 @@ def _wrap(text: str, width: int) -> list[str]: if cur: lines.append(" ".join(cur)) return lines + + +def _wrap_to_width(c: Canvas, text: str, font: str, size: float, max_width: float) -> list[str]: + words, lines, cur = text.split(), [], [] + for word in words: + test = " ".join(cur + [word]) if cur else word + if c.stringWidth(test, font, size) <= max_width: + cur.append(word) + else: + if cur: + lines.append(" ".join(cur)) + cur = [word] + if cur: + lines.append(" ".join(cur)) + return lines diff --git a/src/alt_syrup/panels/front_panel.py b/src/alt_syrup/panels/front_panel.py index fbdffe6..fccd9b3 100644 --- a/src/alt_syrup/panels/front_panel.py +++ b/src/alt_syrup/panels/front_panel.py @@ -42,9 +42,14 @@ def render_front_panel( y = _center(c, f"{product['servings_per_container']} SERVINGS", cx, y, "Helvetica", typo["servings"], WARM_OFF_WHITE, 1.3) _center(c, product["net_contents"], cx, safe.y + 6, "Helvetica", typo["net_contents"], WARM_OFF_WHITE, 1.2) + # Age gate — secondary retail cue, text only + age_gate = brand["brand"].get("age_gate", "21+") + age_size = max(typo["net_contents"] - 1.5, 5.5) + _center(c, age_gate, cx, safe.y + 15, "Helvetica", age_size, WARM_OFF_WHITE, 1.1) + # Statement of identity — secondary, bottom area c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica", typo["net_contents"] - 1.5) stmt = brand["brand"]["statement_of_identity"] tw = c.stringWidth(stmt, "Helvetica", typo["net_contents"] - 1.5) - c.drawString(cx - tw / 2, safe.y + 18, stmt) + c.drawString(cx - tw / 2, safe.y + 22, stmt) diff --git a/src/alt_syrup/panels/supplement_facts.py b/src/alt_syrup/panels/supplement_facts.py index d3b3aa2..4b19961 100644 --- a/src/alt_syrup/panels/supplement_facts.py +++ b/src/alt_syrup/panels/supplement_facts.py @@ -14,7 +14,7 @@ def render_supplement_facts( ) -> None: """FDA Supplement Facts format — vector lines and text only.""" x, y, w, h = zone.x, zone.y, zone.width, zone.height - body = typo.get("supplement_body", 5.0) + body = max(typo.get("supplement_body", 5.5), 5.5) heading = typo.get("supplement_heading", 6.0) c.setFillColor(WARM_OFF_WHITE) @@ -56,5 +56,5 @@ def render_supplement_facts( c.setLineWidth(1) c.line(x + 3, y + 10, x + w - 3, y + 10) - c.setFont("Helvetica", body - 1.2) + c.setFont("Helvetica", max(body - 0.5, 5.0)) c.drawString(x + 3, y + 2, "† Daily Value not established.") From 4fb6f4a01a5479ff67b20d0982c755d7413f9c9a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Jun 2026 21:21:26 +0000 Subject: [PATCH 3/3] fix: execute ALT syrup label correction pass for retail production - Master footer alignment with divider, 21+, and +20% Delta-9 statement - Larger QR zone with expanded quiet zone and separated website - Mango deep amber and strawberry deep red accent refinement - Back-panel clipping, section dividers, wrapped ingredients - Supplement Facts divider alignment and minimum type sizes Co-authored-by: ebyron357 --- config/syrup/brand.yaml | 8 ++- config/syrup/flavors.yaml | 4 +- src/alt_syrup/colors.py | 5 +- src/alt_syrup/layout.py | 12 ++-- src/alt_syrup/panels/back_panel.py | 91 +++++++++++++++++------- src/alt_syrup/panels/front_panel.py | 50 +++++++++---- src/alt_syrup/panels/supplement_facts.py | 45 ++++++------ 7 files changed, 143 insertions(+), 72 deletions(-) diff --git a/config/syrup/brand.yaml b/config/syrup/brand.yaml index c828f97..1eccd05 100644 --- a/config/syrup/brand.yaml +++ b/config/syrup/brand.yaml @@ -27,9 +27,11 @@ colors: champagne_gold: cmyk: [0, 15, 35, 15] deep_amber: - cmyk: [0, 45, 75, 25] + cmyk: [0, 38, 62, 28] berry_accent: cmyk: [25, 55, 30, 10] + strawberry_accent: + cmyk: [18, 72, 48, 22] citrus_accent: cmyk: [0, 25, 55, 10] @@ -40,6 +42,8 @@ typography: thc_per_serving: 8.0 servings: 7.5 net_contents: 7.0 + statement_of_identity: 6.6 + age_gate: 5.0 panel_heading: 6.5 panel_body: 6.0 supplement_heading: 6.0 @@ -75,7 +79,7 @@ qr_section: - "LAB RESULTS" - "INGREDIENTS" - "PRODUCT INFO" - quiet_zone_ratio: 0.12 + quiet_zone_ratio: 0.14 warning_panel: heading: "WARNING:" diff --git a/config/syrup/flavors.yaml b/config/syrup/flavors.yaml index 71456da..8b35b37 100644 --- a/config/syrup/flavors.yaml +++ b/config/syrup/flavors.yaml @@ -9,12 +9,12 @@ flavors: - id: mango name: "MANGO" display_name: "Mango" - accent_color: citrus_accent + accent_color: deep_amber - id: strawberry name: "STRAWBERRY" display_name: "Strawberry" - accent_color: berry_accent + accent_color: strawberry_accent - id: grape name: "GRAPE" diff --git a/src/alt_syrup/colors.py b/src/alt_syrup/colors.py index 559c371..1a011b1 100644 --- a/src/alt_syrup/colors.py +++ b/src/alt_syrup/colors.py @@ -10,13 +10,16 @@ def cmyk(c: float, m: float, y: float, k: float) -> CMYKColor: MATTE_BLACK = cmyk(0, 0, 0, 100) WARM_OFF_WHITE = cmyk(0, 3, 8, 4) CHAMPAGNE_GOLD = cmyk(0, 15, 35, 15) -DEEP_AMBER = cmyk(0, 45, 75, 25) +DEEP_AMBER = cmyk(0, 38, 62, 28) BERRY_ACCENT = cmyk(25, 55, 30, 10) +STRAWBERRY_ACCENT = cmyk(18, 72, 48, 22) CITRUS_ACCENT = cmyk(0, 25, 55, 10) +DIVIDER = cmyk(0, 2, 5, 35) ACCENT_MAP = { "champagne_gold": CHAMPAGNE_GOLD, "deep_amber": DEEP_AMBER, "berry_accent": BERRY_ACCENT, + "strawberry_accent": STRAWBERRY_ACCENT, "citrus_accent": CITRUS_ACCENT, } diff --git a/src/alt_syrup/layout.py b/src/alt_syrup/layout.py index 3a9d91f..4d67b9f 100644 --- a/src/alt_syrup/layout.py +++ b/src/alt_syrup/layout.py @@ -63,12 +63,12 @@ def build_layout() -> SyrupLayout: x0 = safe_back.x w = safe_back.width - qr_zone = Rect(x0, safe_back.y, w * 0.58, h * 0.15) - barcode_zone = Rect(x0 + w * 0.58, safe_back.y, w * 0.42, h * 0.15) - warning_zone = Rect(x0, safe_back.y + h * 0.15, w, h * 0.23) - supplement_zone = Rect(x0, safe_back.y + h * 0.38, w * 0.48, h * 0.24) - ingredients_zone = Rect(x0 + w * 0.50, safe_back.y + h * 0.38, w * 0.50, h * 0.24) - directions_zone = Rect(x0, safe_back.y + h * 0.62, w, h * 0.15) + qr_zone = Rect(x0, safe_back.y, w * 0.62, h * 0.18) + barcode_zone = Rect(x0 + w * 0.62, safe_back.y, w * 0.38, h * 0.18) + warning_zone = Rect(x0, safe_back.y + h * 0.18, w, h * 0.22) + supplement_zone = Rect(x0, safe_back.y + h * 0.40, w * 0.48, h * 0.23) + ingredients_zone = Rect(x0 + w * 0.50, safe_back.y + h * 0.40, w * 0.50, h * 0.23) + directions_zone = Rect(x0, safe_back.y + h * 0.63, w, h * 0.14) responsible_zone = Rect(x0, safe_back.y + h * 0.77, w, h * 0.14) lot_zone = Rect(x0, safe_back.y + h * 0.91, w, h * 0.09) diff --git a/src/alt_syrup/panels/back_panel.py b/src/alt_syrup/panels/back_panel.py index f2502b5..741596d 100644 --- a/src/alt_syrup/panels/back_panel.py +++ b/src/alt_syrup/panels/back_panel.py @@ -1,4 +1,4 @@ -"""Back panel — standardized compliance sections, no filler.""" +"""Back panel — standardized compliance sections, master alignment.""" import io @@ -6,8 +6,8 @@ from reportlab.lib.utils import ImageReader from reportlab.pdfgen.canvas import Canvas -from ..colors import MATTE_BLACK, WARM_OFF_WHITE -from ..layout import SyrupLayout +from ..colors import DIVIDER, MATTE_BLACK, WARM_OFF_WHITE +from ..layout import Rect, SyrupLayout from .supplement_facts import render_supplement_facts @@ -19,6 +19,19 @@ def _lot_size(typo: dict) -> float: return max(typo.get("panel_body", 6.0) - 0.5, 5.5) +def _clip_zone(c: Canvas, zone: Rect) -> None: + c.saveState() + p = c.beginPath() + p.rect(zone.x, zone.y, zone.width, zone.height) + c.clipPath(p, stroke=0, fill=0) + + +def _section_divider(c: Canvas, zone: Rect) -> None: + c.setStrokeColor(DIVIDER) + c.setLineWidth(0.35) + c.line(zone.x, zone.y + zone.height, zone.x + zone.width, zone.y + zone.height) + + def render_back_panel( c: Canvas, layout: SyrupLayout, @@ -32,45 +45,50 @@ def render_back_panel( _render_qr(c, layout, brand, compliance, typo) _render_barcode(c, layout, compliance) - _render_directions(c, layout, brand, typo) - _render_ingredients(c, layout, compliance, typo) - render_supplement_facts(c, layout.supplement_zone, compliance["supplement_facts"], typo) + _section_divider(c, layout.warning_zone) _render_warnings(c, layout, brand, compliance, typo) + render_supplement_facts(c, layout.supplement_zone, compliance["supplement_facts"], typo) + _render_ingredients(c, layout, compliance, typo) + _section_divider(c, layout.directions_zone) + _render_directions(c, layout, brand, typo) + _section_divider(c, layout.responsible_zone) _render_responsible_party(c, layout, brand, typo) + _section_divider(c, layout.lot_zone) _render_lot(c, layout, compliance, typo) def _render_qr(c: Canvas, layout: SyrupLayout, brand: dict, compliance: dict, typo: dict) -> None: zone = layout.qr_zone body = _body_size(typo) - website_band = body * 1.6 + website_band = body * 1.75 content_h = zone.height - website_band - quiet_ratio = brand["qr_section"].get("quiet_zone_ratio", 0.12) - qr_size = min(content_h * 0.82, zone.width * 0.44) - quiet = qr_size * quiet_ratio + quiet_ratio = brand["qr_section"].get("quiet_zone_ratio", 0.14) + qr_size = min(content_h * 0.90, zone.width * 0.50) + quiet = max(qr_size * quiet_ratio, 2) qr_x = zone.x + quiet - qr_y = zone.y + website_band + max((content_h - qr_size) / 2, quiet) + qr_y = zone.y + website_band + max((content_h - qr_size) / 2, quiet * 0.5) url = compliance.get("qr_url", f"https://{brand['brand']['website']}") - qr = qrcode.QRCode(version=1, box_size=10, border=4) + qr = qrcode.QRCode(version=1, box_size=12, border=4, error_correction=qrcode.constants.ERROR_CORRECT_M) qr.add_data(url) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buf = io.BytesIO() img.save(buf, format="PNG") buf.seek(0) - c.drawImage(ImageReader(buf), qr_x, qr_y, qr_size, qr_size, mask="auto") + c.drawImage(ImageReader(buf), qr_x, qr_y, qr_size, qr_size) tx = qr_x + qr_size + quiet - ty = zone.y + zone.height - website_band - 2 + ty = zone.y + zone.height - website_band - 1 c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"] - 0.5) for line in brand["qr_section"]["heading_lines"]: c.drawString(tx, ty, line) - ty -= typo["panel_heading"] * 0.95 + ty -= typo["panel_heading"] * 0.92 c.setFont("Helvetica", body) - c.drawString(zone.x, zone.y + 1, brand["brand"]["website"]) + site = brand["brand"]["website"] + c.drawString(zone.x, zone.y + 2, site) def _render_barcode(c: Canvas, layout: SyrupLayout, compliance: dict) -> None: @@ -106,55 +124,70 @@ def _upc_pattern(digits: str) -> str: def _render_directions(c: Canvas, layout: SyrupLayout, brand: dict, typo: dict) -> None: zone = layout.directions_zone body = _body_size(typo) + _clip_zone(c, zone) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, brand["directions"]["heading"]) - y -= typo["panel_heading"] * 1.15 + y -= typo["panel_heading"] * 1.12 c.setFont("Helvetica", body) for line in brand["directions"]["lines"]: + if y < zone.y + body: + break c.drawString(zone.x, y, line) - y -= body * 1.05 + y -= body * 1.04 + c.restoreState() def _render_ingredients(c: Canvas, layout: SyrupLayout, compliance: dict, typo: dict) -> None: zone = layout.ingredients_zone body = _body_size(typo) + _clip_zone(c, zone) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, "INGREDIENTS:") - y -= typo["panel_heading"] * 1.05 + y -= typo["panel_heading"] * 1.02 c.setFont("Helvetica", body) text = compliance.get("ingredients", "") if not text: text = ", ".join(compliance.get("ingredients_lines", [])) - for line in _wrap_to_width(c, text, "Helvetica", body, zone.width - 2): + for line in _wrap_to_width(c, text, "Helvetica", body, zone.width - 4): + if y < zone.y + body: + break c.drawString(zone.x, y, line) - y -= body * 1.08 + y -= body * 1.06 + c.restoreState() def _render_warnings(c: Canvas, layout: SyrupLayout, brand: dict, compliance: dict, typo: dict) -> None: zone = layout.warning_zone body = _body_size(typo) + _clip_zone(c, zone) y = zone.y + zone.height - typo["panel_heading"] c.setFillColor(WARM_OFF_WHITE) c.setFont("Helvetica-Bold", typo["panel_heading"]) c.drawString(zone.x, y, brand["warning_panel"]["heading"]) - y -= typo["panel_heading"] * 1.1 + y -= typo["panel_heading"] * 1.08 c.setFont("Helvetica", body) for line in brand["warning_panel"]["lines"]: + if y < zone.y + body: + break c.drawString(zone.x, y, line) - y -= body * 1.02 + y -= body * 1.0 for sw in compliance.get("state_warnings", [])[:2]: - for part in _wrap(sw, 36): + for part in _wrap(sw, 34): + if y < zone.y + body: + break c.drawString(zone.x, y, part) - y -= body * 1.02 + y -= body * 1.0 + c.restoreState() def _render_responsible_party(c: Canvas, layout: SyrupLayout, brand: dict, typo: dict) -> None: zone = layout.responsible_zone body = _body_size(typo) + _clip_zone(c, zone) rp = brand["responsible_party"] y = zone.y + zone.height - body c.setFillColor(WARM_OFF_WHITE) @@ -164,8 +197,11 @@ def _render_responsible_party(c: Canvas, layout: SyrupLayout, brand: dict, typo: rp["manufactured_for_label"], rp["manufactured_for"], *rp["address_lines"], ]: + if y < zone.y + body: + break c.drawString(zone.x, y, line) - y -= body * 0.92 + y -= body * 0.90 + c.restoreState() def _render_lot(c: Canvas, layout: SyrupLayout, compliance: dict, typo: dict) -> None: @@ -176,7 +212,8 @@ def _render_lot(c: Canvas, layout: SyrupLayout, compliance: dict, typo: dict) -> lot = compliance.get("lot_number", "") best = compliance.get("best_by", "") c.drawString(zone.x, zone.y + 2, f"Lot: {lot}" if lot else "Lot:") - c.drawString(zone.x + 80, zone.y + 2, f"Best By: {best}" if best else "Best By:") + best_x = zone.x + zone.width * 0.52 + c.drawString(best_x, zone.y + 2, f"Best By: {best}" if best else "Best By:") def _wrap(text: str, width: int) -> list[str]: diff --git a/src/alt_syrup/panels/front_panel.py b/src/alt_syrup/panels/front_panel.py index fccd9b3..740814d 100644 --- a/src/alt_syrup/panels/front_panel.py +++ b/src/alt_syrup/panels/front_panel.py @@ -1,8 +1,8 @@ -"""Front panel — LOCKED hierarchy, improved spacing.""" +"""Front panel — LOCKED hierarchy, master footer alignment.""" from reportlab.pdfgen.canvas import Canvas -from ..colors import ACCENT_MAP, CHAMPAGNE_GOLD, MATTE_BLACK, WARM_OFF_WHITE +from ..colors import ACCENT_MAP, CHAMPAGNE_GOLD, DIVIDER, MATTE_BLACK, WARM_OFF_WHITE from ..layout import SyrupLayout @@ -14,6 +14,39 @@ def _center(c: Canvas, text: str, cx: float, y: float, font: str, size: float, c return y - size * lh +def _render_master_footer( + c: Canvas, + safe, + cx: float, + brand: dict, + product: dict, + typo: dict, +) -> None: + """Shared footer structure — ALT ORIGINAL master alignment for all SKUs.""" + net_y = safe.y + 6 + age_y = safe.y + 14 + stmt_y = safe.y + 22 + divider_y = safe.y + 28 + + c.setStrokeColor(DIVIDER) + c.setLineWidth(0.4) + inset = 10 + c.line(safe.x + inset, divider_y, safe.x + safe.width - inset, divider_y) + + _center(c, product["net_contents"], cx, net_y, "Helvetica", typo["net_contents"], WARM_OFF_WHITE, 1.2) + + age_gate = brand["brand"].get("age_gate", "21+") + age_size = typo.get("age_gate", 5.0) + _center(c, age_gate, cx, age_y, "Helvetica", age_size, WARM_OFF_WHITE, 1.0) + + stmt = brand["brand"]["statement_of_identity"] + stmt_size = typo.get("statement_of_identity", 6.6) + c.setFillColor(WARM_OFF_WHITE) + c.setFont("Helvetica", stmt_size) + tw = c.stringWidth(stmt, "Helvetica", stmt_size) + c.drawString(cx - tw / 2, stmt_y, stmt) + + def render_front_panel( c: Canvas, layout: SyrupLayout, @@ -40,16 +73,5 @@ def render_front_panel( y = _center(c, f"{product['total_thc_mg']} MG THC", cx, y, "Helvetica-Bold", typo["thc_total"], WARM_OFF_WHITE, 1.35) y = _center(c, f"{product['thc_per_serving_mg']} MG THC PER SERVING", cx, y, "Helvetica-Bold", typo["thc_per_serving"], accent, 1.3) y = _center(c, f"{product['servings_per_container']} SERVINGS", cx, y, "Helvetica", typo["servings"], WARM_OFF_WHITE, 1.3) - _center(c, product["net_contents"], cx, safe.y + 6, "Helvetica", typo["net_contents"], WARM_OFF_WHITE, 1.2) - # Age gate — secondary retail cue, text only - age_gate = brand["brand"].get("age_gate", "21+") - age_size = max(typo["net_contents"] - 1.5, 5.5) - _center(c, age_gate, cx, safe.y + 15, "Helvetica", age_size, WARM_OFF_WHITE, 1.1) - - # Statement of identity — secondary, bottom area - c.setFillColor(WARM_OFF_WHITE) - c.setFont("Helvetica", typo["net_contents"] - 1.5) - stmt = brand["brand"]["statement_of_identity"] - tw = c.stringWidth(stmt, "Helvetica", typo["net_contents"] - 1.5) - c.drawString(cx - tw / 2, safe.y + 22, stmt) + _render_master_footer(c, safe, cx, brand, product, typo) diff --git a/src/alt_syrup/panels/supplement_facts.py b/src/alt_syrup/panels/supplement_facts.py index 4b19961..6e6d5fb 100644 --- a/src/alt_syrup/panels/supplement_facts.py +++ b/src/alt_syrup/panels/supplement_facts.py @@ -1,10 +1,12 @@ -"""Supplement Facts — vector panel, no raster typography.""" +"""Supplement Facts — vector panel, aligned dividers.""" from reportlab.pdfgen.canvas import Canvas from ..colors import MATTE_BLACK, WARM_OFF_WHITE from ..layout import Rect +_PAD = 4 + def render_supplement_facts( c: Canvas, @@ -16,45 +18,48 @@ def render_supplement_facts( x, y, w, h = zone.x, zone.y, zone.width, zone.height body = max(typo.get("supplement_body", 5.5), 5.5) heading = typo.get("supplement_heading", 6.0) + inner_l = x + _PAD + inner_r = x + w - _PAD c.setFillColor(WARM_OFF_WHITE) c.rect(x, y, w, h, fill=1, stroke=0) c.setFillColor(MATTE_BLACK) + c.setStrokeColor(MATTE_BLACK) ty = y + h - heading - 2 c.setFont("Helvetica-Bold", heading + 0.5) - c.drawString(x + 3, ty, "Supplement Facts") + c.drawString(inner_l, ty, "Supplement Facts") - c.setLineWidth(2) - c.line(x + 3, ty - 3, x + w - 3, ty - 3) + c.setLineWidth(1.5) + c.line(inner_l, ty - 3, inner_r, ty - 3) ty -= heading + 1 c.setFont("Helvetica", body) - c.drawString(x + 3, ty, f"Serving Size {supplement['serving_size']}") - ty -= body * 1.15 - c.drawString(x + 3, ty, f"Servings Per Container {supplement['servings_per_container']}") + c.drawString(inner_l, ty, f"Serving Size {supplement['serving_size']}") + ty -= body * 1.12 + c.drawString(inner_l, ty, f"Servings Per Container {supplement['servings_per_container']}") - c.setLineWidth(3) - c.line(x + 3, ty - 3, x + w - 3, ty - 3) + c.setLineWidth(2.5) + c.line(inner_l, ty - 3, inner_r, ty - 3) ty -= body + 3 c.setFont("Helvetica-Bold", body) - c.drawString(x + 3, ty, "Amount Per Serving") - ty -= body * 1.2 + c.drawString(inner_l, ty, "Amount Per Serving") + ty -= body * 1.15 c.setFont("Helvetica", body) ingredient = supplement["active_ingredient"] amount = supplement["amount_per_serving"] - c.drawString(x + 3, ty, f"{ingredient}") - c.drawRightString(x + w - 3, ty, amount) - ty -= body * 1.1 + c.drawString(inner_l, ty, f"{ingredient}") + c.drawRightString(inner_r, ty, amount) + ty -= body * 1.08 for other in supplement.get("other_ingredients", []): - c.drawString(x + 3, ty, other["name"]) - c.drawRightString(x + w - 3, ty, other.get("amount", "")) - ty -= body * 1.05 + c.drawString(inner_l, ty, other["name"]) + c.drawRightString(inner_r, ty, other.get("amount", "")) + ty -= body * 1.04 - c.setLineWidth(1) - c.line(x + 3, y + 10, x + w - 3, y + 10) + c.setLineWidth(0.75) + c.line(inner_l, y + 12, inner_r, y + 12) c.setFont("Helvetica", max(body - 0.5, 5.0)) - c.drawString(x + 3, y + 2, "† Daily Value not established.") + c.drawString(inner_l, y + 3, "† Daily Value not established.")