← Files VeraARCHIVED FILE

modules/comunicazione-professionale/scripts/render_visuals.py

59 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/usr/bin/env python3
"""Render accepted studio-formatted visual and circular artifacts."""

from __future__ import annotations

import argparse
import html
import logging
import os
import re
import tempfile
from functools import lru_cache
from pathlib import Path
from typing import Any

from fontTools.ttLib import TTFont as FontToolsTTFont
from PIL import Image, ImageDraw, ImageFont
from pypdf import PdfReader
from reportlab.lib.colors import HexColor
from reportlab.lib.enums import TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
    BaseDocTemplate,
    Frame,
    PageTemplate,
    Paragraph,
    Spacer,
)
from workflow_core import (
    PLUGIN_ROOT,
    atomic_write_json,
    atomic_write_text,
    canonical_digest,
    creative_token_application,
    file_digest,
    load_json,
    recompute_contribution_digest,
    require_accepted_reviews,
    utc_now,
    validate_input_integrity,
    verify_creative_direction_decision,
    workflow_lock,
)

__all__ = ["render_visuals", "main"]

LOGGER = logging.getLogger(__name__)
WIDTH = 1080
HEIGHT = 1350
MARGIN = 92
FONT_ROOT = PLUGIN_ROOT / "assets" / "fonts"
FONT_PATHS = {
    "regular": FONT_ROOT / "InstrumentSans-Regular.ttf",
    "semibold": FONT_ROOT / "InstrumentSans-SemiBold.ttf",
    "bold": FONT_ROOT / "InstrumentSans-Bold.ttf",
}
INTERNAL_ID_PATTERN = re.compile(r"\b(?:SRC|HIST|CLAIM)-[A-Za-z0-9_.-]+\b")
SOURCE_HEADINGS = {
    "en": "Sources",
    "fr": "Sources",
    "de": "Quellen",
    "es": "Fuentes",
}


def _font(weight: str, size: int) -> ImageFont.FreeTypeFont:
    return ImageFont.truetype(str(FONT_PATHS[weight]), size=size)


@lru_cache(maxsize=8)
def _font_codepoints(path: str) -> frozenset[int]:
    """Return the exact cmap for one bundled font asset."""

    font = FontToolsTTFont(path, lazy=True)
    try:
        return frozenset((font.getBestCmap() or {}).keys())
    finally:
        font.close()


def _require_supported_text(text: str, *, weight: str, label: str) -> None:
    """Reject missing glyphs before any partial visual files are written."""

    supported = _font_codepoints(str(FONT_PATHS[weight]))
    missing = sorted(
        {
            character
            for character in text
            if not character.isspace() and ord(character) not in supported
        },
        key=ord,
    )
    if missing:
        details = ", ".join(f"U+{ord(char):04X} {char!r}" for char in missing)
        raise ValueError(f"{label} uses unsupported font glyphs: {details}")


def _identity_visible(placement: str, *, index: int, total: int) -> bool:
    """Apply the accepted Studio identity-placement contract exactly."""

    if placement == "every_slide":
        return True
    if placement == "cover_and_close":
        return index in {1, total}
    if placement == "close_only":
        return index == total
    raise ValueError(f"Unsupported carousel identity placement: {placement}")


def _validate_visual_text_contract(
    slides: list[dict[str, Any]],
    *,
    studio_name: str,
    identity_placement: str,
) -> None:
    """Enforce mechanical public-surface integrity before rendering.

    These checks are deterministic because exact identifier leakage, duplicate
    identity strings, and font coverage are mechanically verifiable. Editorial
    usefulness and overlap with the post remain model-led review questions.
    """

    total = len(slides)
    for index, slide in enumerate(slides, start=1):
        fields = [
            ("eyebrow", slide["eyebrow"], "semibold"),
            ("title", slide["title"], "bold"),
            ("body", slide["body"], "regular"),
            ("highlight", slide["highlight"], "bold"),
            ("source note", slide["source_note"], "regular"),
            *[("bullet", bullet, "regular") for bullet in slide["bullets"]],
        ]
        for field, value, weight in fields:
            if INTERNAL_ID_PATTERN.search(value):
                raise ValueError(
                    f"Visual slide {index} {field} exposes an internal identifier"
                )
            _require_supported_text(
                value,
                weight=weight,
                label=f"Visual slide {index} {field}",
            )
        show_identity = _identity_visible(
            identity_placement,
            index=index,
            total=total,
        )
        identity_mentions = int(show_identity) + sum(
            value.casefold().count(studio_name.casefold())
            for _, value, _ in fields
            if studio_name
        )
        if identity_mentions > 1:
            raise ValueError(
                f"Visual slide {index} repeats Studio identity {identity_mentions} times"
            )
        if show_identity:
            _require_supported_text(
                studio_name,
                weight="semibold",
                label=f"Visual slide {index} Studio identity",
            )


def _wrap(
    draw: ImageDraw.ImageDraw, text: str, font: ImageFont.FreeTypeFont, width: int
) -> list[str]:
    def split_word(word: str) -> list[str]:
        pieces: list[str] = []
        current = ""
        for character in word:
            candidate = current + character
            if current and draw.textlength(candidate, font=font) > width:
                pieces.append(current)
                current = character
            else:
                current = candidate
            if draw.textlength(current, font=font) > width:
                raise ValueError("One glyph exceeds the visual text width")
        if current:
            pieces.append(current)
        return pieces

    lines: list[str] = []
    for paragraph in str(text).splitlines() or [""]:
        words = [
            piece
            for word in paragraph.split()
            for piece in (
                [word]
                if draw.textlength(word, font=font) <= width
                else split_word(word)
            )
        ]
        if not words:
            lines.append("")
            continue
        current = words[0]
        for word in words[1:]:
            candidate = f"{current} {word}"
            if draw.textlength(candidate, font=font) <= width:
                current = candidate
            else:
                lines.append(current)
                current = word
        lines.append(current)
    if any(draw.textlength(line, font=font) > width for line in lines):
        raise ValueError("Wrapped visual line exceeds the safe width")
    return lines


def _fit_lines(
    draw: ImageDraw.ImageDraw,
    text: str,
    *,
    weight: str,
    max_width: int,
    max_height: int,
    start_size: int,
    min_size: int,
    spacing_ratio: float = 0.18,
) -> tuple[ImageFont.FreeTypeFont, list[str], int]:
    for size in range(start_size, min_size - 1, -2):
        font = _font(weight, size)
        lines = _wrap(draw, text, font, max_width)
        spacing = max(4, int(size * spacing_ratio))
        line_height = int(size * 1.14)
        height = len(lines) * line_height + max(0, len(lines) - 1) * spacing
        if height <= max_height:
            return font, lines, spacing
    raise ValueError("Text cannot fit visual contract without clipping")


def _draw_lines(
    draw: ImageDraw.ImageDraw,
    xy: tuple[int, int],
    lines: list[str],
    *,
    font: ImageFont.FreeTypeFont,
    fill: str,
    spacing: int,
) -> int:
    x, y = xy
    line_height = int(font.size * 1.14)
    for line in lines:
        draw.text((x, y), line, font=font, fill=fill)
        y += line_height + spacing
    return y


def _logo_path(source_register: dict[str, Any]) -> Path | None:
    logo = source_register.get("brand_logo")
    if not isinstance(logo, dict):
        return None
    path = Path(str(logo.get("snapshot_path", "")))
    return path if path.is_file() else None


def _draw_logo(
    image: Image.Image,
    path: Path | None,
    *,
    x: int,
    y: int,
    max_width: int,
    max_height: int,
) -> None:
    if path is None:
        return
    with Image.open(path) as original:
        logo = original.convert("RGBA")
        logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
        image.alpha_composite(logo, (x, y))


def _draw_text_block(
    draw: ImageDraw.ImageDraw,
    text: str,
    *,
    x: int,
    y: int,
    width: int,
    height: int,
    weight: str,
    start_size: int,
    min_size: int,
    fill: str,
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]],
    spacing_ratio: float = 0.18,
) -> int:
    """Fit and draw one exact text field, returning its lower edge."""

    font, lines, spacing = _fit_lines(
        draw,
        text,
        weight=weight,
        max_width=width,
        max_height=height,
        start_size=start_size,
        min_size=min_size,
        spacing_ratio=spacing_ratio,
    )
    bottom = _draw_lines(
        draw,
        (x, y),
        lines,
        font=font,
        fill=fill,
        spacing=spacing,
    )
    measured_lines.extend((line, font) for line in lines)
    return bottom


def _draw_header(
    draw: ImageDraw.ImageDraw,
    slide: dict[str, Any],
    *,
    index: int,
    total: int,
    primary: str,
    accent: str,
    inverted: bool = False,
) -> None:
    """Draw the restrained carousel navigation header."""

    header_color = "#FFFFFF" if inverted else primary
    counter = f"{index:02d}/{total:02d}"
    counter_font = _font("regular", 23)
    counter_width = draw.textlength(counter, font=counter_font)
    eyebrow_width = WIDTH - 2 * MARGIN - int(counter_width) - 38
    eyebrow_font = None
    for size in range(25, 17, -1):
        candidate = _font("semibold", size)
        if draw.textlength(slide["eyebrow"].upper(), font=candidate) <= eyebrow_width:
            eyebrow_font = candidate
            break
    if eyebrow_font is None:
        raise ValueError("Carousel eyebrow cannot fit navigation header")
    draw.text(
        (MARGIN, 54),
        slide["eyebrow"].upper(),
        font=eyebrow_font,
        fill=header_color,
    )
    draw.text(
        (WIDTH - MARGIN - counter_width, 56),
        counter,
        font=counter_font,
        fill=header_color,
    )
    draw.rectangle((MARGIN, 104, WIDTH - MARGIN, 110), fill=accent)


def _draw_indexed_rows(
    draw: ImageDraw.ImageDraw,
    bullets: list[str],
    *,
    x: int,
    y: int,
    width: int,
    max_bottom: int,
    primary: str,
    accent: str,
    text: str,
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]],
    boxed: bool = False,
    row_marker: str = "numeral",
) -> int:
    """Draw exact bullet copy as an editorial register."""

    if not bullets:
        return y
    available = max_bottom - y
    row_height = available // len(bullets)
    if row_height < 92:
        raise ValueError("Indexed rows cannot fit visual contract without clipping")
    number_font = _font("semibold", 22)
    for row_index, bullet in enumerate(bullets, start=1):
        row_top = y + (row_index - 1) * row_height
        row_bottom = row_top + row_height - 12
        if boxed:
            draw.rectangle(
                (x, row_top, x + width, row_bottom),
                outline=primary,
                width=2,
            )
            number_x = x + 24
            copy_x = x + 88
            copy_width = width - 116
        else:
            draw.rectangle((x, row_top, x + width, row_top + 2), fill=primary)
            number_x = x
            copy_x = x + 72
            copy_width = width - 72
        marker_top = row_top + 25
        if row_marker == "numeral":
            draw.text(
                (number_x, marker_top),
                f"{row_index:02d}",
                font=number_font,
                fill=accent,
            )
        elif row_marker == "circle":
            draw.ellipse(
                (number_x, marker_top, number_x + 30, marker_top + 30),
                fill=accent,
            )
        elif row_marker == "bar":
            draw.rectangle(
                (number_x, marker_top, number_x + 9, marker_top + 42),
                fill=accent,
            )
        else:
            raise ValueError(
                f"Unsupported Creative Production row marker: {row_marker}"
            )
        _draw_text_block(
            draw,
            bullet,
            x=copy_x,
            y=row_top + 22,
            width=copy_width,
            height=row_height - 40,
            weight="regular",
            start_size=30,
            min_size=22,
            fill=text,
            measured_lines=measured_lines,
            spacing_ratio=0.10,
        )
    return y + len(bullets) * row_height


def _draw_dual_gate(
    draw: ImageDraw.ImageDraw,
    bullets: list[str],
    *,
    y: int,
    max_bottom: int,
    primary: str,
    accent: str,
    text: str,
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]],
    row_marker: str = "numeral",
) -> int:
    """Draw two exact conditions as a connected, two-gate sequence."""

    if len(bullets) != 2:
        raise ValueError("dual_gate layout requires exactly two bullet fields")
    line_x = MARGIN + 34
    row_height = (max_bottom - y) // 2
    if row_height < 145:
        raise ValueError("dual_gate layout cannot fit without clipping")
    draw.rectangle((line_x - 1, y + 44, line_x + 1, max_bottom - 44), fill=primary)
    number_font = _font("semibold", 21)
    for row_index, bullet in enumerate(bullets, start=1):
        row_top = y + (row_index - 1) * row_height
        label = f"{row_index:02d}"
        if row_marker == "circle":
            draw.ellipse(
                (line_x - 24, row_top + 18, line_x + 24, row_top + 66),
                fill=accent,
            )
            label_width = draw.textlength(label, font=number_font)
            draw.text(
                (line_x - label_width / 2, row_top + 28),
                label,
                font=number_font,
                fill="#FFFFFF",
            )
        elif row_marker == "bar":
            draw.rectangle(
                (line_x - 8, row_top + 18, line_x + 8, row_top + 66),
                fill=accent,
            )
        elif row_marker == "numeral":
            label_width = draw.textlength(label, font=number_font)
            draw.text(
                (line_x - label_width / 2, row_top + 28),
                label,
                font=number_font,
                fill=accent,
            )
        else:
            raise ValueError(
                f"Unsupported Creative Production row marker: {row_marker}"
            )
        _draw_text_block(
            draw,
            bullet,
            x=MARGIN + 100,
            y=row_top + 12,
            width=WIDTH - 2 * MARGIN - 100,
            height=row_height - 28,
            weight="regular",
            start_size=31,
            min_size=23,
            fill=text,
            measured_lines=measured_lines,
            spacing_ratio=0.10,
        )
    return y + 2 * row_height


def _draw_route_comparison(
    draw: ImageDraw.ImageDraw,
    bullets: list[str],
    *,
    y: int,
    max_bottom: int,
    primary: str,
    accent: str,
    text: str,
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]],
    row_marker: str = "numeral",
) -> int:
    """Draw two exact routes side by side with an optional shared conclusion."""

    if len(bullets) not in {2, 3}:
        raise ValueError("route_comparison layout requires two or three bullet fields")
    gutter = 36
    column_width = (WIDTH - 2 * MARGIN - gutter) // 2
    comparison_bottom = max_bottom - (150 if len(bullets) == 3 else 0)
    if comparison_bottom - y < 220:
        raise ValueError("route_comparison layout cannot fit without clipping")
    for column_index, bullet in enumerate(bullets[:2]):
        column_x = MARGIN + column_index * (column_width + gutter)
        draw.rectangle((column_x, y, column_x + column_width, y + 8), fill=accent)
        marker_y = y + 28
        if row_marker == "numeral":
            draw.text(
                (column_x, marker_y),
                f"0{column_index + 1}",
                font=_font("semibold", 22),
                fill=primary,
            )
        elif row_marker == "circle":
            draw.ellipse(
                (column_x, marker_y, column_x + 30, marker_y + 30),
                fill=accent,
            )
        elif row_marker == "bar":
            draw.rectangle(
                (column_x, marker_y, column_x + 9, marker_y + 42),
                fill=accent,
            )
        else:
            raise ValueError(
                f"Unsupported Creative Production row marker: {row_marker}"
            )
        _draw_text_block(
            draw,
            bullet,
            x=column_x,
            y=y + 82,
            width=column_width,
            height=comparison_bottom - y - 100,
            weight="regular",
            start_size=31,
            min_size=22,
            fill=text,
            measured_lines=measured_lines,
            spacing_ratio=0.10,
        )
    draw.rectangle(
        (
            MARGIN + column_width + gutter // 2,
            y,
            MARGIN + column_width + gutter // 2 + 2,
            comparison_bottom,
        ),
        fill=primary,
    )
    if len(bullets) == 3:
        draw.rectangle(
            (MARGIN, comparison_bottom + 22, WIDTH - MARGIN, comparison_bottom + 25),
            fill=primary,
        )
        _draw_text_block(
            draw,
            bullets[2],
            x=MARGIN,
            y=comparison_bottom + 48,
            width=WIDTH - 2 * MARGIN,
            height=max_bottom - comparison_bottom - 48,
            weight="semibold",
            start_size=28,
            min_size=22,
            fill=primary,
            measured_lines=measured_lines,
            spacing_ratio=0.08,
        )
    return max_bottom


def _draw_source_footer(
    draw: ImageDraw.ImageDraw,
    slide: dict[str, Any],
    *,
    primary: str,
    studio_name: str,
    identity_visible: bool,
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]],
) -> tuple[int, str]:
    """Draw the exact public source note and optional Studio identity."""

    footer_y = HEIGHT - 112
    draw.rectangle((MARGIN, footer_y - 24, WIDTH - MARGIN, footer_y - 21), fill=primary)
    sources = slide["source_note"].strip()
    footer_width = WIDTH - 2 * MARGIN
    show_footer_identity = identity_visible
    if sources:
        source_font, source_lines, source_spacing = _fit_lines(
            draw,
            sources,
            weight="regular",
            max_width=int(footer_width * (0.62 if show_footer_identity else 1.0)),
            max_height=58,
            start_size=22,
            min_size=15,
        )
        _draw_lines(
            draw,
            (MARGIN, footer_y),
            source_lines,
            font=source_font,
            fill=primary,
            spacing=source_spacing,
        )
        measured_lines.extend((line, source_font) for line in source_lines)
    if show_footer_identity:
        studio_font, studio_lines, studio_spacing = _fit_lines(
            draw,
            studio_name,
            weight="semibold",
            max_width=int(footer_width * 0.34),
            max_height=60,
            start_size=20,
            min_size=15,
        )
        studio_width = max(
            (draw.textlength(line, font=studio_font) for line in studio_lines),
            default=0,
        )
        _draw_lines(
            draw,
            (int(WIDTH - MARGIN - studio_width), footer_y),
            studio_lines,
            font=studio_font,
            fill=primary,
            spacing=studio_spacing,
        )
        measured_lines.extend((line, studio_font) for line in studio_lines)
    return footer_y, sources


def _render_slide(
    slide: dict[str, Any],
    *,
    index: int,
    total: int,
    brand: dict[str, str],
    studio_name: str,
    logo_path: Path | None,
    identity_visible: bool,
    creative_tokens: dict[str, str] | None,
    output_path: Path,
) -> dict[str, Any]:
    background = brand["background_color"]
    primary = brand["primary_color"]
    accent = brand["accent_color"]
    text = brand["text_color"]
    image = Image.new("RGBA", (WIDTH, HEIGHT), background)
    draw = ImageDraw.Draw(image)
    row_marker = "numeral"
    rhythm_gap = 0
    if creative_tokens is not None:
        frame = creative_tokens["frame_style"]
        rule = creative_tokens["rule_style"]
        accent_geometry = creative_tokens["accent_geometry"]
        header = creative_tokens["header_treatment"]
        rhythm = creative_tokens["spacing_rhythm"]
        row_marker = creative_tokens["row_marker"]
        rhythm_gap = {"compact": -14, "balanced": 0, "airy": 18}[rhythm]
        stroke = 4
        if frame == "hairline":
            draw.rectangle((22, 22, WIDTH - 22, HEIGHT - 22), outline=primary, width=2)
        elif frame == "inset":
            draw.rectangle(
                (34, 34, WIDTH - 34, HEIGHT - 34), outline=primary, width=stroke
            )
        line_y = 102 if rule != "offset" else 118
        draw.line((MARGIN, line_y, WIDTH - MARGIN, line_y), fill=accent, width=stroke)
        if rule == "double":
            draw.line(
                (MARGIN, line_y + 12, WIDTH - MARGIN, line_y + 12),
                fill=primary,
                width=max(1, stroke // 2),
            )
        if accent_geometry == "corner_stamp":
            draw.rectangle((WIDTH - 116, 0, WIDTH, 116), fill=accent)
        elif accent_geometry == "side_bar":
            draw.rectangle((0, 150, stroke + 8, HEIGHT - 190), fill=accent)
        else:
            draw.rectangle((MARGIN, 126, MARGIN + 210, 126 + stroke), fill=accent)
        if header == "outline":
            draw.rectangle(
                (MARGIN - 20, 58, WIDTH - MARGIN + 20, 146),
                outline=primary,
                width=2,
            )
        elif header == "split":
            draw.rectangle((0, 0, WIDTH // 3, 22), fill=primary)
            draw.rectangle((WIDTH // 3, 0, WIDTH, 22), fill=accent)
    measured_lines: list[tuple[str, ImageFont.FreeTypeFont]] = []
    layout_variant = slide["layout_variant"]
    content_limit = HEIGHT - 156
    inverted_header = layout_variant in {"editorial_cover", "threshold_notice"}
    if inverted_header:
        draw.rectangle((0, 0, WIDTH, 840), fill=primary)
    _draw_header(
        draw,
        slide,
        index=index,
        total=total,
        primary=primary,
        accent=accent,
        inverted=inverted_header,
    )

    if identity_visible and logo_path is not None:
        _draw_logo(
            image,
            logo_path,
            x=WIDTH - MARGIN - 200,
            y=128,
            max_width=200,
            max_height=70,
        )

    if layout_variant == "editorial_cover":
        y = _draw_text_block(
            draw,
            slide["title"],
            x=MARGIN,
            y=190,
            width=WIDTH - 2 * MARGIN,
            height=500,
            weight="bold",
            start_size=84,
            min_size=50,
            fill="#FFFFFF",
            measured_lines=measured_lines,
        )
        if slide["highlight"]:
            y = _draw_text_block(
                draw,
                slide["highlight"],
                x=MARGIN,
                y=y + 20,
                width=WIDTH - 2 * MARGIN,
                height=120,
                weight="bold",
                start_size=74,
                min_size=44,
                fill=accent,
                measured_lines=measured_lines,
            )
        y = _draw_text_block(
            draw,
            slide["body"],
            x=MARGIN,
            y=900 + rhythm_gap,
            width=WIDTH - 2 * MARGIN,
            height=230,
            weight="regular",
            start_size=38,
            min_size=27,
            fill=text,
            measured_lines=measured_lines,
        )
    elif layout_variant == "threshold_notice":
        y = _draw_text_block(
            draw,
            slide["title"],
            x=MARGIN,
            y=170,
            width=WIDTH - 2 * MARGIN,
            height=280,
            weight="bold",
            start_size=66,
            min_size=42,
            fill="#FFFFFF",
            measured_lines=measured_lines,
        )
        y = _draw_text_block(
            draw,
            slide["highlight"],
            x=MARGIN,
            y=y + 26 + rhythm_gap,
            width=WIDTH - 2 * MARGIN,
            height=150,
            weight="bold",
            start_size=94,
            min_size=54,
            fill=accent,
            measured_lines=measured_lines,
        )
        _draw_text_block(
            draw,
            slide["body"],
            x=MARGIN,
            y=y + 40,
            width=WIDTH - 2 * MARGIN,
            height=185,
            weight="regular",
            start_size=32,
            min_size=24,
            fill="#FFFFFF",
            measured_lines=measured_lines,
        )
        y = _draw_indexed_rows(
            draw,
            slide["bullets"],
            x=MARGIN,
            y=875 + rhythm_gap,
            width=WIDTH - 2 * MARGIN,
            max_bottom=content_limit,
            primary=primary,
            accent=accent,
            text=text,
            measured_lines=measured_lines,
            row_marker=row_marker,
        )
    else:
        y = _draw_text_block(
            draw,
            slide["title"],
            x=MARGIN,
            y=170,
            width=WIDTH - 2 * MARGIN,
            height=280,
            weight="bold",
            start_size=70,
            min_size=42,
            fill=primary,
            measured_lines=measured_lines,
        )
        if slide["highlight"]:
            y = _draw_text_block(
                draw,
                slide["highlight"],
                x=MARGIN,
                y=y + 18 + rhythm_gap,
                width=WIDTH - 2 * MARGIN,
                height=120,
                weight="bold",
                start_size=78,
                min_size=46,
                fill=accent,
                measured_lines=measured_lines,
            )
        y = _draw_text_block(
            draw,
            slide["body"],
            x=MARGIN,
            y=y + 26 + rhythm_gap,
            width=WIDTH - 2 * MARGIN,
            height=210,
            weight="regular",
            start_size=36,
            min_size=25,
            fill=text,
            measured_lines=measured_lines,
        )
        y += 26 + rhythm_gap
        if layout_variant == "dual_gate":
            y = _draw_dual_gate(
                draw,
                slide["bullets"],
                y=y,
                max_bottom=content_limit,
                primary=primary,
                accent=accent,
                text=text,
                measured_lines=measured_lines,
                row_marker=row_marker,
            )
        elif layout_variant == "route_comparison":
            y = _draw_route_comparison(
                draw,
                slide["bullets"],
                y=y,
                max_bottom=content_limit,
                primary=primary,
                accent=accent,
                text=text,
                measured_lines=measured_lines,
                row_marker=row_marker,
            )
        else:
            y = _draw_indexed_rows(
                draw,
                slide["bullets"],
                x=MARGIN,
                y=y,
                width=WIDTH - 2 * MARGIN,
                max_bottom=content_limit,
                primary=primary,
                accent=accent,
                text=text,
                measured_lines=measured_lines,
                boxed=layout_variant == "evidence_dossier",
                row_marker=row_marker,
            )

    if y > content_limit:
        raise ValueError(f"Slide {index} content exceeds safe visual area")
    footer_y, sources = _draw_source_footer(
        draw,
        slide,
        primary=primary,
        studio_name=studio_name,
        identity_visible=identity_visible and logo_path is None,
        measured_lines=measured_lines,
    )

    output_path.parent.mkdir(parents=True, exist_ok=True)
    image.convert("RGB").save(output_path, format="PNG", optimize=True)
    return {
        "overflow_free": True,
        "safe_area": [MARGIN, 54, WIDTH - MARGIN, footer_y - 44],
        "content_bottom": y,
        "footer_top": footer_y - 24,
        "layout_variant": layout_variant,
        "max_line_width_px": max(
            (draw.textlength(line, font=font) for line, font in measured_lines),
            default=0,
        ),
        "available_width_px": WIDTH - 2 * MARGIN,
        "identity_visible": identity_visible,
        "public_source_note": sources,
        "internal_id_leakage": False,
        "creative_direction_tokens": creative_tokens,
    }


def _pdf_font_names(font_family: str) -> dict[str, str]:
    """Resolve the approved Studio font family to reproducible PDF font names."""

    if font_family == "Instrument Sans":
        _register_pdf_fonts()
        return {
            "regular": "InstrumentSans",
            "semibold": "InstrumentSans-Semibold",
            "bold": "InstrumentSans-Bold",
        }
    if font_family == "Helvetica":
        return {
            "regular": "Helvetica",
            "semibold": "Helvetica-Bold",
            "bold": "Helvetica-Bold",
        }
    if font_family == "Times":
        return {
            "regular": "Times-Roman",
            "semibold": "Times-Bold",
            "bold": "Times-Bold",
        }
    raise ValueError(f"Unsupported approved Studio font family: {font_family}")


def _register_pdf_fonts() -> None:
    pdfmetrics.registerFont(TTFont("InstrumentSans", str(FONT_PATHS["regular"])))
    pdfmetrics.registerFont(
        TTFont("InstrumentSans-Semibold", str(FONT_PATHS["semibold"]))
    )
    pdfmetrics.registerFont(TTFont("InstrumentSans-Bold", str(FONT_PATHS["bold"])))


def _paragraphs(text: str, style: ParagraphStyle) -> list[Any]:
    rows: list[Any] = []
    for paragraph in [part.strip() for part in text.split("\n\n") if part.strip()]:
        rows.extend(
            (
                Paragraph(html.escape(paragraph).replace("\n", "<br/>"), style),
                Spacer(1, 2.5 * mm),
            )
        )
    return rows


def _wrap_pdf_text(
    value: str, *, font_name: str, font_size: float, max_width: float
) -> list[str]:
    """Wrap exact PDF text by measured glyph width, including long tokens."""

    def pieces(word: str) -> list[str]:
        rows: list[str] = []
        current = ""
        for character in word:
            candidate = current + character
            if (
                current
                and pdfmetrics.stringWidth(candidate, font_name, font_size) > max_width
            ):
                rows.append(current)
                current = character
            else:
                current = candidate
            if pdfmetrics.stringWidth(current, font_name, font_size) > max_width:
                raise ValueError("One PDF glyph exceeds the available width")
        if current:
            rows.append(current)
        return rows

    words = [
        part
        for word in value.split()
        for part in (
            [word]
            if pdfmetrics.stringWidth(word, font_name, font_size) <= max_width
            else pieces(word)
        )
    ]
    if not words:
        return [""]
    lines: list[str] = []
    current = words[0]
    for word in words[1:]:
        candidate = f"{current} {word}"
        if pdfmetrics.stringWidth(candidate, font_name, font_size) <= max_width:
            current = candidate
        else:
            lines.append(current)
            current = word
    lines.append(current)
    if any(
        pdfmetrics.stringWidth(line, font_name, font_size) > max_width for line in lines
    ):
        raise ValueError("Wrapped PDF line exceeds the available width")
    return lines


def _normalized_pdf_text(value: str) -> str:
    return " ".join(value.split())


def _pdf_match_key(value: str) -> str:
    """Normalize extractor-added spacing around punctuation for presence checks."""

    return "".join(character for character in value.casefold() if character.isalnum())


def _render_circular_pdf(
    draft: dict[str, Any],
    *,
    brand: dict[str, str],
    studio_profile: dict[str, Any],
    studio_name: str,
    reference_date: str,
    language: str,
    logo_path: Path | None,
    output_path: Path,
) -> dict[str, Any]:
    document_profile = studio_profile["document"]
    font_names = _pdf_font_names(document_profile["font_family"])
    layout = document_profile["layout"]
    primary = HexColor(brand["primary_color"])
    accent = HexColor(brand["accent_color"])
    text_color = HexColor(brand["text_color"])
    use_rail = bool(
        document_profile["use_contact_rail"] and document_profile["contact_rail_lines"]
    )
    page_width, page_height = A4
    rail_rows: list[tuple[str, list[str]]] = []
    if use_rail:
        rail_width = max(8 * mm, (layout["contact_rail_width_mm"] - 8) * mm)
        rail_height = 0.0
        for index, line in enumerate(document_profile["contact_rail_lines"]):
            font_role = "semibold" if index == 0 else "regular"
            wrapped = (
                _wrap_pdf_text(
                    line,
                    font_name=font_names[font_role],
                    font_size=8.5,
                    max_width=rail_width,
                )
                if line
                else [""]
            )
            rail_rows.append((font_role, wrapped))
            rail_height += len(wrapped) * 11 + (4 if not line else 0)
        if rail_height > page_height - 76 * mm:
            raise ValueError("Studio contact rail cannot fit without omission")

    header_lines: list[str] = []
    if logo_path is None:
        header_lines = _wrap_pdf_text(
            studio_name,
            font_name=font_names["bold"],
            font_size=18,
            max_width=page_width
            - (layout["left_margin_mm"] + layout["right_margin_mm"]) * mm,
        )
        if len(header_lines) > 2:
            raise ValueError("Studio name cannot fit the circular header")

    class CircularDocument(BaseDocTemplate):
        pass

    output_path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(
        prefix=f".{output_path.stem}.", suffix=".pdf", dir=output_path.parent
    )
    os.close(descriptor)
    temporary_path = Path(temporary_name)
    doc = CircularDocument(
        str(temporary_path),
        pagesize=A4,
        leftMargin=layout["left_margin_mm"] * mm,
        rightMargin=layout["right_margin_mm"] * mm,
        topMargin=layout["top_margin_mm"] * mm,
        bottomMargin=layout["bottom_margin_mm"] * mm,
        title=draft["title"],
        author=studio_name,
    )
    horizontal_margins = (layout["left_margin_mm"] + layout["right_margin_mm"]) * mm
    vertical_margins = (layout["top_margin_mm"] + layout["bottom_margin_mm"]) * mm
    first_width = (
        page_width
        - horizontal_margins
        - (layout["contact_rail_width_mm"] * mm if use_rail else 0)
    )
    first_frame = Frame(
        layout["left_margin_mm"] * mm,
        layout["bottom_margin_mm"] * mm,
        first_width,
        page_height - vertical_margins,
        id="first",
    )
    later_frame = Frame(
        layout["left_margin_mm"] * mm,
        layout["bottom_margin_mm"] * mm,
        page_width - horizontal_margins,
        page_height - vertical_margins,
        id="later",
    )

    def draw_page(canvas: Any, document: Any, *, first: bool) -> None:
        canvas.saveState()
        if logo_path:
            canvas.drawImage(
                str(logo_path),
                layout["left_margin_mm"] * mm,
                page_height - (layout["top_margin_mm"] - 8) * mm,
                width=layout["logo_width_mm"] * mm,
                height=layout["logo_height_mm"] * mm,
                preserveAspectRatio=True,
                anchor="w",
                mask="auto",
            )
        else:
            canvas.setFont(font_names["bold"], 18)
            canvas.setFillColor(primary)
            header_y = page_height - (layout["top_margin_mm"] - 13) * mm
            for line in header_lines:
                canvas.drawString(layout["left_margin_mm"] * mm, header_y, line)
                header_y -= 20
        canvas.setStrokeColor(accent)
        canvas.setLineWidth(layout["rule_width_pt"])
        canvas.line(
            layout["left_margin_mm"] * mm,
            page_height - (layout["top_margin_mm"] - 3) * mm,
            page_width - layout["right_margin_mm"] * mm,
            page_height - (layout["top_margin_mm"] - 3) * mm,
        )
        canvas.setFont(font_names["regular"], 8)
        canvas.setFillColor(text_color)
        footer = document_profile["footer_pattern"].replace(
            "{page}", str(document.page)
        )
        footer_lines = _wrap_pdf_text(
            footer or f"pag. {document.page}",
            font_name=font_names["regular"],
            font_size=8,
            max_width=page_width
            - (layout["left_margin_mm"] + layout["right_margin_mm"]) * mm,
        )
        if len(footer_lines) > 2:
            raise ValueError("Studio footer cannot fit the circular page")
        footer_y = max(5, layout["bottom_margin_mm"] - 9) * mm
        for line in footer_lines:
            canvas.drawCentredString(page_width / 2, footer_y, line)
            footer_y += 9
        if first and use_rail:
            rail_x = (
                page_width
                - (layout["right_margin_mm"] + layout["contact_rail_width_mm"]) * mm
            )
            canvas.setStrokeColor(HexColor("#D9D9D9"))
            canvas.setLineWidth(1.4)
            canvas.line(rail_x, 22 * mm, rail_x, page_height - 43 * mm)
            text = canvas.beginText(rail_x + 4 * mm, page_height - 48 * mm)
            text.setFont(font_names["semibold"], 8.5)
            text.setFillColor(primary)
            for font_role, wrapped_lines in rail_rows:
                if wrapped_lines == [""]:
                    text.moveCursor(0, 4)
                    continue
                text.setFont(font_names[font_role], 8.5)
                for wrapped_line in wrapped_lines:
                    text.textLine(wrapped_line)
            canvas.drawText(text)
        canvas.restoreState()

    doc.addPageTemplates(
        [
            PageTemplate(
                id="first",
                frames=[first_frame],
                onPage=lambda c, d: draw_page(c, d, first=True),
                autoNextPageTemplate="later",
            ),
            PageTemplate(
                id="later",
                frames=[later_frame],
                onPage=lambda c, d: draw_page(c, d, first=False),
            ),
        ]
    )
    styles = getSampleStyleSheet()
    normal = ParagraphStyle(
        "Body",
        parent=styles["BodyText"],
        fontName=font_names["regular"],
        fontSize=layout["body_font_size_pt"],
        leading=layout["body_leading_pt"],
        textColor=text_color,
        alignment=TA_LEFT,
        spaceAfter=2 * mm,
    )
    meta = ParagraphStyle("Meta", parent=normal, fontSize=9, leading=12)
    subject = ParagraphStyle(
        "Subject",
        parent=normal,
        fontName=font_names["bold"],
        fontSize=layout["subject_font_size_pt"],
        leading=layout["subject_font_size_pt"] * 1.23,
        textColor=primary,
        spaceBefore=3 * mm,
        spaceAfter=6 * mm,
    )
    heading = ParagraphStyle(
        "Heading",
        parent=normal,
        fontName=font_names["bold"],
        fontSize=layout["heading_font_size_pt"],
        leading=layout["heading_font_size_pt"] * 1.27,
        spaceBefore=4 * mm,
        spaceAfter=2 * mm,
        keepWithNext=True,
    )
    bullet_style = ParagraphStyle(
        "Bullet",
        parent=normal,
        leftIndent=7 * mm,
        firstLineIndent=-4 * mm,
        spaceAfter=1.5 * mm,
    )
    recipient_line = (
        draft.get("recipient_line") or document_profile["recipient_pattern"]
    )
    date_line = draft.get("date_line") or document_profile["date_pattern"].replace(
        "{date}", reference_date
    )
    circular_line = " ".join(
        part
        for part in (
            document_profile["circular_label"],
            draft.get("circular_number", ""),
        )
        if part
    )
    subject_line = " ".join(
        part
        for part in (
            document_profile["subject_prefix"],
            draft.get("subject") or draft["title"],
        )
        if part
    )
    story: list[Any] = [
        Paragraph(
            html.escape(recipient_line),
            meta,
        ),
        Paragraph(
            html.escape(date_line),
            meta,
        ),
        Spacer(1, 3 * mm),
        Paragraph(
            html.escape(circular_line),
            ParagraphStyle(
                "Circular",
                parent=meta,
                fontName=font_names["semibold"],
                textColor=primary,
            ),
        ),
        Paragraph(
            html.escape(subject_line),
            subject,
        ),
    ]
    story.extend(_paragraphs(draft["body"], normal))
    for index, section in enumerate(draft["sections"], start=1):
        section_title = section["heading"]
        if document_profile["section_style"] == "numbered_uppercase":
            section_title = f"{index}  {section_title.upper()}"
        elif document_profile["section_style"] == "numbered_sentence":
            section_title = f"{index}  {section_title}"
        story.append(Paragraph(html.escape(section_title), heading))
        story.extend(_paragraphs(section["body"], normal))
        if section["bullets"]:
            story.extend(
                Paragraph(f"- {html.escape(item)}", bullet_style)
                for item in section["bullets"]
            )
    source_lines = [
        (
            f"{note['text']} — {note['public_url']}"
            if note.get("public_url") and note["public_url"] not in note["text"]
            else note["text"]
        )
        for note in draft["public_source_notes"]
    ]
    source_heading = ""
    if source_lines:
        language_key = language.replace("_", "-").split("-", maxsplit=1)[0].lower()
        source_heading = (
            studio_profile["website"]["source_heading"]
            if language_key == "it"
            else SOURCE_HEADINGS.get(language_key, "Sources")
        )
        story.append(Paragraph(html.escape(source_heading), heading))
        story.extend(Paragraph(html.escape(line), normal) for line in source_lines)
    story.extend(
        (Spacer(1, 6 * mm), Paragraph(html.escape(document_profile["closing"]), normal))
    )
    for line in document_profile["signature_lines"] or [studio_name]:
        story.append(
            Paragraph(
                html.escape(line),
                ParagraphStyle(
                    "Signature", parent=normal, fontName=font_names["semibold"]
                ),
            )
        )
    try:
        doc.build(story)
        reader = PdfReader(str(temporary_path))
        extracted = _normalized_pdf_text(
            "\n".join(page.extract_text() or "" for page in reader.pages)
        )
        required_text = [
            recipient_line,
            date_line,
            circular_line,
            subject_line,
            draft["body"],
            *(
                value
                for section in draft["sections"]
                for value in (
                    section["heading"],
                    section["body"],
                    *section["bullets"],
                )
                if value
            ),
            source_heading,
            *source_lines,
            document_profile["closing"],
            *(line for line in document_profile["signature_lines"] if line),
            *(header_lines if logo_path is None else []),
            *(
                line
                for line in document_profile["contact_rail_lines"]
                if use_rail and line
            ),
            *(
                document_profile["footer_pattern"].replace("{page}", str(page))
                or f"pag. {page}"
                for page in range(1, len(reader.pages) + 1)
            ),
        ]
        missing = [
            value
            for value in required_text
            if _pdf_match_key(value) not in _pdf_match_key(extracted)
        ]
        if missing:
            raise ValueError(
                "Circular PDF omitted reviewed text: " + ", ".join(missing[:3])
            )
        os.chmod(temporary_path, 0o600)
        temporary_path.replace(output_path)
        return {
            "overflow_free": True,
            "page_layout_engine": "reportlab_platypus",
            "page_count": len(reader.pages),
            "text_extraction_verified": True,
            "contact_rail_exact": True,
            "manual_regions_fit": True,
            "silent_truncation": False,
        }
    finally:
        if temporary_path.exists():
            temporary_path.unlink()


def _preview_html(entries: list[dict[str, Any]], *, title: str, language: str) -> str:
    cards = "\n".join(
        f'<figure><img src="{html.escape(Path(row["path"]).name)}" alt="Slide {index}"><figcaption>{html.escape(row["kind"])} · {row["width"]} × {row["height"]}</figcaption></figure>'
        for index, row in enumerate(entries, start=1)
        if row["kind"] == "carousel_slide"
    )
    return f"""<!doctype html>
<html lang="{html.escape(language.replace('_', '-'), quote=True)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{html.escape(title)}</title><style>
body{{margin:0;background:#f5f6f8;color:#171816;font-family:"Instrument Sans",Arial,sans-serif}}
main{{max-width:1280px;margin:auto;padding:48px}}h1{{font-size:32px;font-weight:650;margin:0 0 32px}}
.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:28px;align-items:start}}
figure{{margin:0;background:#fff;border:1px solid #d9dde4;padding:12px}}img{{display:block;width:100%;height:auto}}
figcaption{{padding:12px 4px 4px;font-size:13px;color:#52606d}}
</style></head><body><main><h1>{html.escape(title)}</h1><div class="grid">{cards}</div></main></body></html>"""


def _visual_review_markdown(
    contribution: dict[str, Any],
    entries: list[dict[str, Any]],
    *,
    identity_placement: str,
    render_state: str,
    profile_status: str,
    logo_present: bool,
) -> str:
    """Expose the exact mechanical facts and required model-led review."""

    story = contribution["visual_story"]
    lines = [
        "# Exact visual quality review",
        "",
        f"- Render state: **{render_state}**",
        f"- Studio format status: **{profile_status}**",
        f"- Official logo asset present: **{str(logo_present).lower()}**",
        f"- Visual decision: **{story['decision']}**",
        f"- Decision reason: {story['decision_reason']}",
        f"- Incremental value over the post: {story['incremental_value']}",
        f"- Studio identity placement: `{identity_placement}`",
        "",
        "## Per-slide contract",
        "",
    ]
    slide_entries = [row for row in entries if row["kind"] == "carousel_slide"]
    for index, (slide, entry) in enumerate(
        zip(story["slides"], slide_entries, strict=True), start=1
    ):
        layout = entry["layout_validation"]
        lines.extend(
            [
                f"### Slide {index:02d}",
                "",
                f"- Job: `{slide['kind']}`",
                f"- Composition: `{slide['layout_variant']}`",
                f"- Reader use: {slide['reader_use']}",
                f"- Relationship to post: `{slide['relationship_to_post']}`",
                f"- Public source note: {slide['source_note'] or 'none'}",
                f"- Studio identity visible: `{str(layout['identity_visible']).lower()}`",
                f"- Exact PNG: `{entry['path']}` · `{entry['sha256']}`",
                "",
            ]
        )
    document_entries = [row for row in entries if row["kind"] == "client_circular_pdf"]
    if document_entries:
        lines.extend(("## Rendered documents", ""))
        for entry in document_entries:
            validation = entry["layout_validation"]
            lines.extend(
                (
                    f"- Exact PDF: `{entry['path']}` · `{entry['sha256']}`",
                    f"- Pages: `{validation['page_count']}`",
                    "- Exact text extraction, contact rail, footer, and manual-region fit: `passed`",
                    "",
                )
            )
    lines.extend(
        [
            "## Required model-led inspection",
            "",
            "Do not accept the rendered output until an editor has opened every exact PNG and answered all of these questions from the visible artifact:",
            "",
            "- Does the carousel add useful detail, structure, comparison, sequence, or a decision aid beyond the post? If it only paraphrases the post, return it.",
            "- Does each slide have one non-trivial job, or do title, highlight, body, and bullets repeat the same proposition?",
            "- Are the source notes meaningful to a reader without access to internal manifests?",
            "- Is Studio identity present only where the accepted profile requires it?",
            "- Are checklists explicitly bounded, without implying that a preliminary screen is a sufficient professional conclusion?",
            "- Is the most prominent number or phrase useful for a decision, rather than decorative emphasis?",
            "- At LinkedIn mobile size, are title, body, source note, and footer still legible and balanced?",
            "- Does the output look authored for this Studio, or like a generic AI carousel?",
            "- For every PDF page: are pagination, header, footer, contact rail, source notes, closing, and signature visibly complete and balanced?",
            "",
            "Mechanical checks cannot answer these questions. Record acceptance only after the model-led inspection is complete.",
            "",
        ]
    )
    return "\n".join(lines)


def render_visuals(run_dir: Path, *, qa_preview: bool = False) -> Path:
    """Render an isolated QA preview or accepted release-candidate visuals."""

    root = run_dir.resolve()
    with workflow_lock(root):
        return _render_visuals_locked(root, qa_preview=qa_preview)


def _render_visuals_locked(root: Path, *, qa_preview: bool) -> Path:
    """Render while one writer owns the run and all integrity bindings hold."""

    validate_input_integrity(root)
    intake = load_json(root / "run_intake.json")
    workbench = load_json(root / "content_workbench.json")
    recompute_contribution_digest(root)
    contribution = workbench["contribution"]
    if contribution["recommendation"] != "publish":
        raise ValueError("No visual rendering for no_publish recommendation")
    if not qa_preview:
        require_accepted_reviews(root, workbench["required_review_scopes"])
    source_register = load_json(root / "source_register.json")
    proposal = contribution.get("studio_profile_proposal")
    if proposal is not None:
        studio_profile = proposal
    else:
        stored = intake.get("studio_profile")
        if not isinstance(stored, dict) or not isinstance(stored.get("payload"), dict):
            raise ValueError("Accepted or stored studio profile required for rendering")
        studio_profile = stored["payload"]["profile"]
    brand = intake["brand_profile"]
    studio_name = brand["studio_name"]
    logo = _logo_path(source_register)
    slides = contribution["visual_story"]["slides"]
    creative_direction = verify_creative_direction_decision(root)
    creative_tokens = creative_direction["tokens"]
    applied_creative_tokens, inapplicable_creative_tokens = creative_token_application(
        creative_tokens, slides
    )
    circular = next(
        (
            draft
            for draft in contribution["channel_drafts"]
            if draft["channel"] == "client_circular"
        ),
        None,
    )
    if not slides and circular is None:
        raise ValueError("Contribution correctly omits visual rendering")
    identity_placement = studio_profile["social"]["carousel_identity_placement"]
    _validate_visual_text_contract(
        slides,
        studio_name=studio_name,
        identity_placement=identity_placement,
    )
    render_state = "qa_preview" if qa_preview else "accepted_semantics"
    profile_status = (
        "unreviewed_run_profile_proposal"
        if proposal is not None and qa_preview
        else (
            "accepted_run_profile"
            if proposal is not None
            else "stored_approved_profile"
        )
    )
    profile_provenance_summary = {
        basis: sum(
            len(record["field_paths"])
            for record in studio_profile["field_provenance"]
            if record["basis"] == basis
        )
        for basis in (
            "observed_history",
            "user_supplied",
            "vera_default_proposal",
        )
    }
    visuals_dir = root / ("visuals-preview" if qa_preview else "visuals")
    manifest_path = root / (
        "visual_preview_manifest.json" if qa_preview else "visual_manifest.json"
    )
    if manifest_path.exists():
        raise ValueError(
            f"{render_state} visual output already exists; supersede the contribution to render a new version"
        )
    visuals_dir.mkdir(exist_ok=True)
    entries: list[dict[str, Any]] = []

    for index, slide in enumerate(slides, start=1):
        output = visuals_dir / f"slide-{index:02d}.png"
        layout_validation = _render_slide(
            slide,
            index=index,
            total=len(slides),
            brand=brand,
            studio_name=studio_name,
            logo_path=logo,
            identity_visible=_identity_visible(
                identity_placement,
                index=index,
                total=len(slides),
            ),
            creative_tokens=creative_tokens,
            output_path=output,
        )
        with Image.open(output) as image:
            width, height = image.size
        entries.append(
            {
                "path": str(output.relative_to(root)),
                "kind": "carousel_slide",
                "width": width,
                "height": height,
                "sha256": file_digest(output),
                "size_bytes": output.stat().st_size,
                "source_ids": slide["source_ids"],
                "reader_use": slide["reader_use"],
                "relationship_to_post": slide["relationship_to_post"],
                "layout_validation": layout_validation,
            }
        )

    if circular is not None:
        circular_path = visuals_dir / "circolare-clienti.pdf"
        circular_validation = _render_circular_pdf(
            circular,
            brand=brand,
            studio_profile=studio_profile,
            studio_name=studio_name,
            reference_date=intake["reference_date"],
            language=intake["language"],
            logo_path=logo,
            output_path=circular_path,
        )
        entries.append(
            {
                "path": str(circular_path.relative_to(root)),
                "kind": "client_circular_pdf",
                "sha256": file_digest(circular_path),
                "size_bytes": circular_path.stat().st_size,
                "source_ids": sorted(
                    {
                        source_id
                        for claim in contribution["claims"]
                        if claim["id"] in circular["claim_ids"]
                        for source_id in claim["source_ids"]
                    }
                ),
                "layout_validation": circular_validation,
            }
        )

    preview = visuals_dir / "visual-preview.html"
    preview_title = contribution["visual_story"]["title"] or (
        circular["title"] if circular else studio_name
    )
    atomic_write_text(
        preview,
        _preview_html(entries, title=preview_title, language=intake["language"]),
    )
    entries.append(
        {
            "path": str(preview.relative_to(root)),
            "kind": "visual_preview_html",
            "sha256": file_digest(preview),
            "size_bytes": preview.stat().st_size,
            "source_ids": [],
            "layout_validation": {"overflow_free": True},
        }
    )
    review_path = visuals_dir / "visual-review.md"
    atomic_write_text(
        review_path,
        _visual_review_markdown(
            contribution,
            entries,
            identity_placement=identity_placement,
            render_state=render_state,
            profile_status=profile_status,
            logo_present=logo is not None,
        ),
    )
    entries.append(
        {
            "path": str(review_path.relative_to(root)),
            "kind": "visual_quality_review_md",
            "sha256": file_digest(review_path),
            "size_bytes": review_path.stat().st_size,
            "source_ids": [],
            "layout_validation": {"overflow_free": True},
        }
    )
    manifest = {
        "schema_version": 1,
        "workflow": "comunicazione-professionale",
        "run_id": intake["run_id"],
        "contribution_digest": workbench["contribution_digest"],
        "rendered_at": utc_now(),
        "render_state": render_state,
        "renderer": "deterministic_pillow_reportlab_v5",
        "creative_direction": {
            key: value for key, value in creative_direction.items() if key != "tokens"
        },
        "font_assets": {name: file_digest(path) for name, path in FONT_PATHS.items()},
        "studio_profile_version": (
            intake["studio_profile"]["payload"]["version"]
            if proposal is None
            else "run_proposal"
        ),
        "quality_gate": {
            "mechanical_checks": {
                "font_coverage": "passed",
                "internal_id_leakage": "passed",
                "identity_duplicates": "passed",
                "overflow": "passed",
                "pdf_text_preservation": "passed",
            },
            "model_led_review_required": True,
            "review_artifact": str(review_path.relative_to(root)),
            "visual_decision": contribution["visual_story"]["decision"],
            "incremental_value": contribution["visual_story"]["incremental_value"],
            "identity_placement": identity_placement,
            "studio_format_status": profile_status,
            "studio_format_provenance_summary": profile_provenance_summary,
            "official_logo_asset_present": logo is not None,
            "creative_tokens_consumed": applied_creative_tokens,
            "creative_tokens_not_applicable": inapplicable_creative_tokens,
        },
        "outputs": entries,
    }
    manifest["manifest_digest"] = canonical_digest(manifest)
    return atomic_write_json(manifest_path, manifest)


def main(argv: list[str] | None = None) -> int:
    """Render accepted visuals."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--run-dir", type=Path, required=True)
    parser.add_argument(
        "--qa-preview",
        action="store_true",
        help="Render an isolated, non-packageable preview before professional review.",
    )
    args = parser.parse_args(argv)
    try:
        path = render_visuals(args.run_dir, qa_preview=args.qa_preview)
    except (OSError, ValueError) as exc:
        LOGGER.error("VISUAL_RENDER_FAILED: %s", exc)
        return 1
    LOGGER.info("Rendered visual package: %s", path)
    return 0


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    raise SystemExit(main())

SHA-256: c07ac4376bd254bcbd249e9e3a8432e1948999cf1678c5dd5cd9233ac03a033c