PosterGen / src /agents /layout_agent.py
Hadlay's picture
First commit
46a8a46
"""
precise layout generation using css box model
"""
import json
from pathlib import Path
from typing import Dict, Any, List, Tuple
from src.state.poster_state import PosterState
from utils.langgraph_utils import LangGraphAgent, extract_json, load_prompt
from utils.src.logging_utils import log_agent_info, log_agent_success, log_agent_error, log_agent_warning
from src.layout.text_height_measurement import measure_text_height
from src.config.poster_config import load_config
class LayoutAgent:
"""creates optimized layouts using css box model"""
def __init__(self):
self.name = "layout_agent"
self.config = load_config()
self.poster_margin = self.config["layout"]["poster_margin"]
self.column_spacing = self.config["layout"]["column_spacing"]
self.title_height_fraction = self.config["layout"]["title_height_fraction"]
self.title_font_family = self.config["typography"]["fonts"]["title"]
self.authors_font_family = self.config["typography"]["fonts"]["authors"]
self.section_title_font_family = self.config["typography"]["fonts"]["section_title"]
self.body_text_font_family = self.config["typography"]["fonts"]["body_text"]
# layout constants
self.layout_constants = self.config["layout_constants"]
self.column_balancing = self.config["column_balancing"]
# debug configuration
self.show_debug_borders = self.config["rendering"]["debug_borders"] ## enable to see section boundaries for debugging
def __call__(self, state: PosterState, mode: str = "initial") -> PosterState:
if mode == "initial":
return self._generate_initial_layout(state)
else:
return self._generate_final_layout(state)
def _generate_initial_layout(self, state: PosterState) -> PosterState:
"""generate initial layout without optimization - direct curator mapping"""
log_agent_info(self.name, "generating initial layout from story board")
try:
story_board = state.get("story_board")
if not story_board:
raise ValueError("missing story_board from curator")
# organize sections from story board for layout creation
sections = story_board["spatial_content_plan"]["sections"]
optimized_layout = self._organize_sections_by_column(sections)
# create layout directly from curator output - no optimization
layout_data = self._create_precise_layout(
story_board=story_board,
optimized_layout=optimized_layout,
state=state
)
# generate column analysis for balancer
column_analysis = self._generate_column_analysis(layout_data, state)
state["initial_layout_data"] = layout_data
state["column_analysis"] = column_analysis
state["current_agent"] = self.name
self._save_initial_layout(state)
log_agent_success(self.name, "initial layout generated")
return state
except Exception as e:
log_agent_error(self.name, f"initial layout error: {e}")
state["errors"].append(f"{self.name}: {e}")
return state
def _generate_final_layout(self, state: PosterState) -> PosterState:
"""generate final layout from optimized story board"""
log_agent_info(self.name, "generating final layout from optimized story board")
try:
optimized_story_board = state.get("optimized_story_board")
if not optimized_story_board:
raise ValueError("missing optimized_story_board from balancer")
# organize sections from optimized story board
sections = optimized_story_board["spatial_content_plan"]["sections"]
organized_layout = self._organize_sections_by_column(sections)
# create final layout from optimized story board
layout_data = self._create_precise_layout(
story_board=optimized_story_board,
optimized_layout=organized_layout,
state=state
)
# generate final column analysis to verify optimization success
final_column_analysis = self._generate_column_analysis(layout_data, state)
# validate final layout
validation = self._validate_precise_layout(layout_data, state["poster_width"], state["poster_height"])
state["design_layout"] = layout_data
state["final_column_analysis"] = final_column_analysis
state["optimized_column_assignment"] = organized_layout["optimized_layout"]["column_assignments"]
state["current_agent"] = self.name
self._save_final_layout(state)
log_agent_success(self.name, "final layout complete")
return state
except Exception as e:
log_agent_error(self.name, f"final layout error: {e}")
state["errors"].append(f"{self.name}: {e}")
return state
def _optimize_column_distribution(self, story_board: Dict, poster_width: int, poster_height: int, config, state) -> Dict:
"""rule-based column distribution for optimal space utilization"""
log_agent_info(self.name, "optimizing column distribution")
# calculate available space
effective_height = poster_height - 2 * self.poster_margin # total height minus margins
title_region_height = effective_height * self.title_height_fraction # 18% of effective height
available_height = effective_height - title_region_height # remaining height for sections
column_width = (poster_width - 2 * self.poster_margin - 2 * self.column_spacing) / 3
# handle new spatial content plan format
if "spatial_content_plan" in story_board:
sections = story_board["spatial_content_plan"]["sections"]
column_distribution = story_board.get("column_distribution", {})
else:
# fallback to old format
sections = story_board.get("story_board", {}).get("sections", [])
column_distribution = {}
# create precise spatial layout using css-like calculations
optimized_layout = self._create_spatial_layout(
sections, column_distribution, available_height, column_width, state
)
log_agent_success(self.name, f"created rule-based optimized layout")
return {
"optimized_layout": {
"column_assignments": optimized_layout,
"strategy": "rule_based_intelligent",
"space_utilization_target": 0.90,
"column_dimensions": {
"width": column_width,
"height": available_height
}
}
}
def _apply_adjustments(self, adjustments: Dict):
"""apply critic-requested adjustments to layout parameters"""
if adjustments.get("increase_spacing"):
log_agent_info(self.name, "increased spacing: adjusting layout constants")
if adjustments.get("reduce_sizes"):
log_agent_info(self.name, "reduced spacing: adjusting layout constants")
if adjustments.get("poster_margin"):
self.poster_margin = adjustments["poster_margin"]
if adjustments.get("column_spacing"):
self.column_spacing = adjustments["column_spacing"]
def _save_initial_layout(self, state: PosterState):
"""save initial layout data"""
output_dir = Path(state["output_dir"]) / "content"
output_dir.mkdir(parents=True, exist_ok=True)
with open(output_dir / "initial_layout_data.json", "w", encoding='utf-8') as f:
json.dump(state.get("initial_layout_data", []), f, indent=2)
with open(output_dir / "column_analysis.json", "w", encoding='utf-8') as f:
json.dump(state.get("column_analysis", {}), f, indent=2)
def _save_final_layout(self, state: PosterState):
"""save final layout data"""
output_dir = Path(state["output_dir"]) / "content"
output_dir.mkdir(parents=True, exist_ok=True)
with open(output_dir / "final_design_layout.json", "w", encoding='utf-8') as f:
json.dump(state.get("design_layout", []), f, indent=2)
with open(output_dir / "optimized_layout.json", "w", encoding='utf-8') as f:
json.dump(state.get("optimized_column_assignment", {}), f, indent=2)
# save final column analysis to show optimization success
if state.get("final_column_analysis"):
with open(output_dir / "final_column_analysis.json", "w", encoding='utf-8') as f:
json.dump(state.get("final_column_analysis", {}), f, indent=2)
def _generate_column_analysis(self, layout_data: List[Dict], state: PosterState) -> Dict:
"""generate detailed column utilization analysis using exact column calculation method"""
poster_width = state["poster_width"]
poster_height = state["poster_height"]
effective_height = poster_height - 2 * self.poster_margin
title_region_height = effective_height * self.title_height_fraction
available_height = effective_height - title_region_height
# calculate precise column x coordinates using global constants
column_width = (poster_width - 2 * self.poster_margin - 2 * self.column_spacing) / 3
left_column_x = self.poster_margin
middle_column_x = self.poster_margin + column_width + self.column_spacing
right_column_x = self.poster_margin + 2 * (column_width + self.column_spacing)
columns = {"left": [], "middle": [], "right": []}
# group elements by column using calculated column boundaries
for element in layout_data:
if element.get("type") == "section_container":
element_x = element.get("x", 0)
# use midpoint boundaries to categorize elements
if element_x < (left_column_x + middle_column_x) / 2:
columns["left"].append(element)
elif element_x < (middle_column_x + right_column_x) / 2:
columns["middle"].append(element)
else:
columns["right"].append(element)
# calculate utilization for each column
column_analysis = {
"available_height": available_height,
"columns": {}
}
for col_name, elements in columns.items():
if elements:
max_bottom = max(elem["y"] + elem["height"] for elem in elements)
min_top = min(elem["y"] for elem in elements)
used_height = max_bottom - min_top
else:
used_height = 0
utilization_rate = used_height / available_height if available_height > 0 else 0
status = "overflow" if utilization_rate > 1.0 else "underutilized" if utilization_rate < 0.7 else "balanced"
column_analysis["columns"][col_name] = {
"utilization_rate": utilization_rate,
"total_height": used_height,
"status": status,
"available_space": max(0, available_height - used_height),
"excess_height": max(0, used_height - available_height)
}
return column_analysis
def _organize_sections_by_column(self, sections: List[Dict]) -> Dict:
"""organize sections by column assignment for layout creation"""
columns = {"left": [], "middle": [], "right": []}
for section in sections:
column = section.get("column_assignment", "left")
if column in columns:
columns[column].append(section)
column_assignments = [
{"column_id": 0, "sections": columns["left"]},
{"column_id": 1, "sections": columns["middle"]},
{"column_id": 2, "sections": columns["right"]}
]
return {
"optimized_layout": {
"column_assignments": column_assignments
}
}
def _create_precise_layout(self, story_board: Dict, optimized_layout: Dict, state: PosterState) -> List[Dict]:
"""create precise layout with exact positioning using measurements"""
layout_elements = []
# poster dimensions
poster_width = state["poster_width"]
poster_height = state["poster_height"]
# calculate layout dimensions
effective_height = poster_height - 2 * self.poster_margin
title_region_height = effective_height * self.title_height_fraction # 18% fixed region
available_height = effective_height - title_region_height # remaining for sections
column_width = (poster_width - 2 * self.poster_margin - 2 * self.column_spacing) / 3
# add title element (still uses actual measured height, not fixed region height)
title_element = self._create_title_element(state, poster_width, title_region_height)
if title_element:
layout_elements.append(title_element)
# add logo elements
logo_elements = self._create_logo_elements(state, poster_width)
layout_elements.extend(logo_elements)
# process each column
column_assignments = optimized_layout.get("optimized_layout", {}).get("column_assignments", [])
for col_idx, column in enumerate(column_assignments):
column_x = self.poster_margin + col_idx * (column_width + self.column_spacing)
column_y = self.poster_margin + title_region_height # fixed at poster_margin + 18%
current_y = column_y
# process each section in this column
for section in column.get("sections", []):
section_start_y = current_y
section_elements = self._create_section_elements(
section, column_x, current_y, column_width, state, available_height
)
# calculate section height from actual element positions
section_height = 0
if section_elements:
# find the bottommost element
max_bottom = 0
for element in section_elements:
element_bottom = element["y"] + element["height"]
max_bottom = max(max_bottom, element_bottom)
section_height = max_bottom - section_start_y
# create section container for layout structure
section_container = {
"type": "section_container",
"x": column_x,
"y": section_start_y,
"width": column_width,
"height": section_height,
"section_id": section.get("section_id", "unknown"),
"importance_level": section.get("importance_level", 2), # importance level for background styling
"priority": 0.1
}
# add debug border only if enabled
if self.show_debug_borders:
section_container["debug_border"] = True
layout_elements.append(section_container)
layout_elements.extend(section_elements)
current_y += section_height + 1.0 # 1" section spacing for stability
log_agent_info(self.name, f"placed section '{section.get('section_id')}' at y={section_start_y:.2f}, height={section_height:.2f}")
return layout_elements
def _create_title_element(self, state: PosterState, poster_width: float, title_height: float) -> Dict:
"""create title element with exact positioning"""
# calculate 2/3 width (2 columns + 1 margin width)
column_width = (poster_width - 2 * self.poster_margin - 2 * self.column_spacing) / 3
title_width = 2 * column_width + self.column_spacing # 2 columns + 1 spacing
# extract title and authors from narrative content
narrative = state.get("narrative_content", {})
meta = narrative.get("meta", {})
poster_title = meta.get("poster_title", state.get('poster_name', 'Title'))
authors = meta.get("authors", state.get('authors', 'Authors'))
return {
"type": "title",
"x": self.poster_margin,
"y": self.poster_margin,
"width": title_width,
"height": title_height - 1.0,
"content": f"{poster_title}\n{authors}",
"font_family": self.title_font_family,
"font_size": 100,
"author_font_size": 72,
"priority": 1.0
}
def _create_logo_elements(self, state: PosterState, poster_width: float) -> List[Dict]:
"""create logo elements with exact positioning"""
elements = []
# get aspect ratio of logos
from PIL import Image
conf_logo_aspect_ratio = self.layout_constants["default_logo_aspect_ratio"]
aff_logo_aspect_ratio = self.layout_constants["default_logo_aspect_ratio"]
if state.get("logo_path") and Path(state["logo_path"]).exists():
with Image.open(state["logo_path"]) as img:
conf_logo_aspect_ratio = img.size[0] / img.size[1]
if state.get("aff_logo_path") and Path(state["aff_logo_path"]).exists():
with Image.open(state["aff_logo_path"]) as img:
aff_logo_aspect_ratio = img.size[0] / img.size[1]
# calculate logo heights based on fit in 1/3 of poster width
column_width = (poster_width - 2 * self.poster_margin - 2 * self.column_spacing) / 3
logo_height = (column_width - 1) / (conf_logo_aspect_ratio + aff_logo_aspect_ratio)
# widths based on aspect ratios
conf_logo_width = logo_height * conf_logo_aspect_ratio
aff_logo_width = logo_height * aff_logo_aspect_ratio
conf_logo_x = poster_width - self.poster_margin - conf_logo_width
aff_logo_x = conf_logo_x - 1.0 - aff_logo_width
if state.get("aff_logo_path"):
elements.append({
"type": "aff_logo",
"x": aff_logo_x,
"y": self.poster_margin,
"width": aff_logo_width,
"height": logo_height,
"priority": 0.9
})
if state.get("logo_path"):
elements.append({
"type": "conf_logo",
"x": conf_logo_x,
"y": self.poster_margin,
"width": conf_logo_width,
"height": logo_height,
"priority": 0.9
})
return elements
def _create_section_elements(self, section: Dict, column_x: float, start_y: float,
column_width: float, state: PosterState, available_height: float = None) -> List[Dict]:
"""create all elements for a section with precise positioning"""
elements = []
current_y = start_y
# enhanced section title with design styling
section_title = section.get("section_title", "")
if section_title:
title_elements = self._create_section_title_design(
section, column_x, current_y, column_width, state
)
elements.extend(title_elements)
# calculate total height used by title and accent elements
title_total_height = max(elem["y"] + elem["height"] - current_y for elem in title_elements)
current_y += title_total_height + self.config["layout"]["title_to_content_spacing"]
# visual assets first (after title, before text)
visual_assets = section.get("visual_assets", [])
for visual_asset in visual_assets:
visual_id = visual_asset.get("visual_id", "")
# apply same padding as text elements
visual_padding = self.config["layout"]["text_padding"]["left_right"] # left/right padding
visual_width = column_width - (2 * visual_padding)
final_visual_width, final_visual_height, scale_factor = self._calculate_visual_height(visual_id, visual_width, state, available_height)
# center the visual within the section (important for scaled visuals)
section_content_width = column_width - (2 * visual_padding)
if final_visual_width < section_content_width:
# center horizontally within the section
visual_x = column_x + visual_padding + (section_content_width - final_visual_width) / 2
else:
# use left alignment if visual fills the section
visual_x = column_x + visual_padding
elements.append({
"type": "visual",
"x": visual_x,
"y": current_y,
"width": final_visual_width,
"height": final_visual_height,
"visual_id": visual_id,
"scale_factor": scale_factor, # for renderer to apply proper scaling
"priority": 0.6,
"id": f"{section.get('section_id')}_{visual_id}",
"font_family": self.body_text_font_family,
"font_color": "#000000",
"font_size": 44,
"line_spacing": 1.0
})
# use the already-scaled height for positioning (no double scaling)
current_y += final_visual_height + self.config["layout"]["visual_spacing"]["below_visual"]
# text content (after visuals)
text_content = section.get("text_content", [])
if text_content:
combined_text = "\n".join(text_content)
text_padding = self.config["layout"]["text_padding"]["left_right"] # consistent with layout positioning
text_measurement = measure_text_height(
text_content=combined_text,
width_inches=column_width - (2 * text_padding),
font_name=self.body_text_font_family,
font_size=44,
line_spacing=1.0
)
text_height = text_measurement["optimal_height"] + 0.1
# apply text padding to match measurement calculation
elements.append({
"type": "text",
"x": column_x + text_padding,
"y": current_y,
"width": column_width - (2 * text_padding),
"height": text_height,
"content": combined_text,
"font_family": self.body_text_font_family,
"font_size": 44,
"font_color": "#000000",
"priority": 0.5,
"id": f"{section.get('section_id')}_text",
"line_spacing": 1.0
})
current_y += text_height + 0.3
return elements
def _create_section_title_design(self, section: Dict, column_x: float, start_y: float, column_width: float, state: PosterState) -> List[Dict]:
"""create section title with colorblock styling"""
elements = []
section_title = section.get("section_title", "")
section_id = section.get("section_id", "")
# get section title design from state
title_design = state.get("section_title_design", {}).get("section_title_design", {})
# find specific section application
section_app = None
for app in title_design.get("section_applications", []):
if app.get("section_id") == section_id:
section_app = app
break
# extract styling information
title_styling = section_app.get("title_styling", {})
accent_styling = section_app.get("accent_styling", {})
# create title textbox positioning
title_padding = self.layout_constants["title_padding"]
title_width = column_width - (2 * title_padding)
base_title_x = column_x + title_padding
# use font size from styling_interfaces
styling_interfaces = state.get("styling_interfaces", {})
section_title_font_size = styling_interfaces.get("font_sizes", {}).get("section_title", 64)
# calculate rectangle dimensions (same as before)
rect_height = section_title_font_size / 72 # convert pt to inches precisely
rect_width = rect_height * 0.618 # golden ratio width
# apply user-requested coordinate modifications
rect_y_offset = 10 / 72 # 10pt converted to inches
title_x_offset = rect_height # offset by rectangle height
# create rectangle background element with y offset
rectangle_element = {
"type": "title_accent_block",
"x": base_title_x,
"y": start_y + rect_y_offset, # user modification: y + 10pt
"width": rect_width,
"height": rect_height,
"color": accent_styling.get("color", "#335f91"),
"priority": 0.7
}
elements.append(rectangle_element)
# adjust title content (add 4 spaces prefix for rectangle_left template)
display_title = " " + section_title
# create title element with x offset using precise font-based height
precise_title_height = section_title_font_size / 72 # pt to inches
title_element = {
"type": "section_title",
"x": base_title_x + title_x_offset, # x + rectangle height
"y": start_y,
"width": title_width,
"height": precise_title_height,
"section_title": display_title,
"font_family": title_styling.get("font_family", self.section_title_font_family),
"font_size": section_title_font_size,
"font_weight": title_styling.get("font_weight", "bold"),
"font_color": title_styling.get("color", "#000000"),
"alignment": title_styling.get("alignment", "left"),
"priority": 0.8
}
elements.append(title_element)
return elements
def _validate_precise_layout(self, layout_data: List[Dict], poster_width: float,
poster_height: float) -> Dict[str, Any]:
"""validate layout for overlaps and overflow"""
issues = []
valid = True
# check for overflow
for element in layout_data:
right_edge = element["x"] + element["width"]
bottom_edge = element["y"] + element["height"]
if right_edge > poster_width:
issues.append(f"Element {element.get('id', 'unknown')} overflows right edge")
valid = False
if bottom_edge > poster_height:
issues.append(f"Element {element.get('id', 'unknown')} overflows bottom edge")
valid = False
# check for overlaps (simplified check)
for i, elem1 in enumerate(layout_data):
for j, elem2 in enumerate(layout_data[i+1:], i+1):
if self._elements_overlap(elem1, elem2):
issues.append(f"Elements {elem1.get('id', 'unknown')} and {elem2.get('id', 'unknown')} overlap")
valid = False
# calculate space utilization
total_used_area = sum(elem["width"] * elem["height"] for elem in layout_data)
total_poster_area = poster_width * poster_height
space_utilization = total_used_area / total_poster_area if total_poster_area > 0 else 0
return {
"valid": valid,
"issues": issues,
"space_utilization": space_utilization,
"total_elements": len(layout_data)
}
def _elements_overlap(self, elem1: Dict, elem2: Dict) -> bool:
"""check if two elements overlap"""
return not (
elem1["x"] + elem1["width"] <= elem2["x"] or
elem2["x"] + elem2["width"] <= elem1["x"] or
elem1["y"] + elem1["height"] <= elem2["y"] or
elem2["y"] + elem2["height"] <= elem1["y"]
)
def _create_spatial_layout(self, sections: List[Dict], column_distribution: Dict,
available_height: float, column_width: float, state: PosterState) -> List[Dict]:
"""create precise spatial layout using css-like calculations"""
log_agent_info(self.name, "creating spatial layout with css-like precision")
# organize sections by spatial assignment
columns = {
"left": {"sections": [], "total_height": 0.0},
"middle": {"sections": [], "total_height": 0.0},
"right": {"sections": [], "total_height": 0.0}
}
for section in sections:
column = section.get("column_assignment", "left")
if column in columns:
columns[column]["sections"].append(section)
log_agent_info(self.name, f"organized sections: left={len(columns['left']['sections'])}, middle={len(columns['middle']['sections'])}, right={len(columns['right']['sections'])}")
# calculate precise heights for each section
for column_name, column_data in columns.items():
for section in column_data["sections"]:
section_height = self._calculate_precise_section_height(section, column_width, state, available_height)
section["calculated_height"] = section_height
column_data["total_height"] += section_height
# return layout in expected format
return [{
"column_id": 0,
"sections": [s for s in sections if s.get("column_assignment") == "left"],
"estimated_height": columns["left"]["total_height"]
}, {
"column_id": 1,
"sections": [s for s in sections if s.get("column_assignment") == "middle"],
"estimated_height": columns["middle"]["total_height"]
}, {
"column_id": 2,
"sections": [s for s in sections if s.get("column_assignment") == "right"],
"estimated_height": columns["right"]["total_height"]
}]
def _calculate_precise_section_height(self, section: Dict, column_width: float, state: PosterState, available_height: float = None) -> float:
"""calculate precise section height using css box model"""
total_height = 0.0
# section title height (if exists)
title = section.get("section_title", "")
if title:
title_padding = self.layout_constants["title_padding"] # consistent with layout positioning
title_measurement = measure_text_height(
text_content=title,
width_inches=column_width - (2 * title_padding), # account for padding
font_name="Helvetica Neue",
font_size=64,
line_spacing=1.0
)
title_height = title_measurement["optimal_height"] + 0.3 # title margin
total_height += title_height
# text content height with fixed line spacing
text_content = section.get("text_content", [])
if text_content:
# join all bullet points with proper paragraph separation
full_text = "\n\n".join(text_content) # double newline between paragraphs
text_padding = self.config["layout"]["text_padding"]["left_right"] # consistent with layout positioning
text_measurement = measure_text_height(
text_content=full_text,
width_inches=column_width - (2 * text_padding), # account for padding
font_name=self.body_text_font_family,
font_size=44,
line_spacing=1.0
)
text_height = text_measurement["optimal_height"] + 0.2 # text margin
total_height += text_height
# visual assets height (fixed aspect ratio)
visual_assets = section.get("visual_assets", [])
for visual in visual_assets:
visual_id = visual.get("visual_id", "")
if visual_id:
visual_padding = self.layout_constants["visual_padding"] # consistent with layout positioning
visual_width = column_width - (2 * visual_padding)
final_visual_width, final_visual_height, scale_factor = self._calculate_visual_height(visual_id, visual_width, state, available_height)
# use the already-scaled height for section sizing (no double scaling)
total_height += final_visual_height + 0.3 # visual margin
# section padding and margins
section_padding = self.layout_constants["section_padding"]
total_height += section_padding
return total_height
def _calculate_visual_height(self, visual_id: str, visual_width: float, state, available_height: float = None) -> tuple:
"""calculate proper visual width and height based on aspect ratio with auto-shrinking for large visuals
returns: (final_width, final_height, scale_factor)
"""
# visual width already accounts for padding (passed from caller)
# get aspect ratio from state data
images = state.get("images", {})
tables = state.get("tables", {})
# better default aspect ratios based on visual type
if visual_id.startswith("table_"):
aspect_ratio = 1.5 # tables are often wider than tall
elif visual_id.startswith("figure_"):
aspect_ratio = 1.2 # figures vary but often slightly wider
else:
aspect_ratio = self.layout_constants["default_logo_aspect_ratio"] # default square
# handle both formats: "figure_1"/"table_1" and "1"/"2" etc.
lookup_id = visual_id
# if visual_id has prefix, extract the number
if visual_id.startswith("figure_"):
lookup_id = visual_id.replace("figure_", "")
elif visual_id.startswith("table_"):
lookup_id = visual_id.replace("table_", "")
# check in appropriate collection first based on visual_id prefix
if visual_id.startswith("table_") and lookup_id in tables:
found_aspect = tables[lookup_id].get("aspect", aspect_ratio)
aspect_ratio = found_aspect
log_agent_info(self.name, f"found visual {visual_id} -> {lookup_id} in tables, aspect={aspect_ratio:.2f}")
elif visual_id.startswith("figure_") and lookup_id in images:
found_aspect = images[lookup_id].get("aspect", aspect_ratio)
aspect_ratio = found_aspect
log_agent_info(self.name, f"found visual {visual_id} -> {lookup_id} in images, aspect={aspect_ratio:.2f}")
elif lookup_id in images:
found_aspect = images[lookup_id].get("aspect", aspect_ratio)
aspect_ratio = found_aspect
log_agent_info(self.name, f"found visual {visual_id} -> {lookup_id} in images, aspect={aspect_ratio:.2f}")
elif lookup_id in tables:
found_aspect = tables[lookup_id].get("aspect", aspect_ratio)
aspect_ratio = found_aspect
log_agent_info(self.name, f"found visual {visual_id} -> {lookup_id} in tables, aspect={aspect_ratio:.2f}")
else:
log_agent_warning(self.name, f"visual {visual_id} (lookup: {lookup_id}) not found in state data, using fallback aspect={aspect_ratio:.2f}")
# debug: log available visual data
log_agent_info(self.name, f"available images: {list(images.keys())}")
log_agent_info(self.name, f"available tables: {list(tables.keys())}")
# calculate original height from aspect ratio
original_height = visual_width / aspect_ratio
# check if shrinking is needed (height > 40% of column height)
scale_factor = 1.0
if available_height and original_height > (available_height * 0.4):
scale_factor = 0.8 # shrink to 80% of original size
log_agent_info(self.name, f"visual {visual_id} too large ({original_height:.2f}\" > 40% of {available_height:.2f}\"), shrinking to 80%")
# apply scaling to both width and height to maintain aspect ratio
final_width = visual_width * scale_factor
final_height = original_height * scale_factor
log_agent_info(self.name, f"visual {visual_id}: orig_w={visual_width:.2f}\", orig_h={original_height:.2f}\", scale={scale_factor:.1f}, final_w={final_width:.2f}\", final_h={final_height:.2f}\"")
# return final width, height and scale factor for rendering
return final_width, final_height, scale_factor
def layout_agent_node(state: PosterState) -> Dict[str, Any]:
result = LayoutAgent()(state)
return {
**state,
"design_layout": result["design_layout"],
"column_assignment": result.get("column_assignment"),
"tokens": result["tokens"],
"current_agent": result["current_agent"],
"errors": result["errors"]
}