""" text height measurement using binary search overflow detection """ from pptx import Presentation from pptx.util import Inches, Pt from pptx.enum.text import MSO_AUTO_SIZE, PP_ALIGN from typing import Dict, Any, List from pathlib import Path from src.config.poster_config import load_config def get_font_file_path(font_name: str) -> str: font_mapping = { "Arial": "fonts/Arial.ttf", "Helvetica Neue": "fonts/HelveticaNeue.ttf", } font_file = font_mapping.get(font_name, "fonts/Arial.ttf") project_root = Path(__file__).parent.parent.parent font_path = project_root / font_file return str(font_path) def measure_text_height(text_content: str, width_inches: float, font_name: str = "Arial", font_size: int = 44, line_spacing: float = 1.0, precision: float = 0.001) -> Dict[str, Any]: """find minimum height for text to fit without font size reduction""" config = load_config() prs = Presentation() config = load_config() slide_layout_index = config["powerpoint"]["slide_layout_blank"] slide = prs.slides.add_slide(prs.slide_layouts[slide_layout_index]) min_height = config["text_measurement"]["min_height"] max_height = config["text_measurement"]["max_height"] tolerance = precision while (max_height - min_height) > tolerance: test_height = (min_height + max_height) / 2 textbox = slide.shapes.add_textbox( left=Inches(config["powerpoint"]["text_frame_positioning"]["default_left"]), top=Inches(config["powerpoint"]["text_frame_positioning"]["default_top"]), width=Inches(width_inches), height=Inches(test_height) ) text_frame = textbox.text_frame text_frame.clear() text_frame.word_wrap = True text_frame.auto_size = MSO_AUTO_SIZE.NONE # use same margins as layout agent for consistent measurement text_frame.margin_left = Inches(config["text_measurement"]["margins"]["left"]) text_frame.margin_right = Inches(config["text_measurement"]["margins"]["right"]) text_frame.margin_top = Inches(config["text_measurement"]["margins"]["top"]) text_frame.margin_bottom = Inches(config["text_measurement"]["margins"]["bottom"]) # process text exactly like renderer: split by single newlines lines = text_content.split('\n') for line_idx, line in enumerate(lines): line = line.strip() if not line: continue # create paragraph for each line (matching renderer behavior) if line_idx == 0 and len(text_frame.paragraphs) > 0: p = text_frame.paragraphs[0] else: p = text_frame.add_paragraph() p.text = line p.alignment = PP_ALIGN.LEFT p.line_spacing = line_spacing # apply font to each paragraph if p.runs: run = p.runs[0] run.font.name = font_name run.font.size = Pt(font_size) original_size = font_size font_reduced = False try: # Use direct font file to bypass cross-platform discovery bug font_file_path = get_font_file_path(font_name) text_frame.fit_text(font_file=font_file_path, max_size=font_size) for paragraph in text_frame.paragraphs: for run in paragraph.runs: if run.font.size and run.font.size.pt < (original_size - 0.5): font_reduced = True break if font_reduced: break except Exception as e: print(f"fit_text error: {e}") font_reduced = True if font_reduced: min_height = test_height else: max_height = test_height # cleanup textbox sp = textbox._element sp.getparent().remove(sp) # calculate newline offset to compensate for pptx rendering discrepancy newline_count = text_content.count('\n') newline_offset = newline_count * (font_size / 72) * config["text_measurement"]["newline_offset_ratio"] final_height = max_height + newline_offset return { "optimal_height": final_height, "text_content": text_content, "width_inches": width_inches, "font_name": font_name, "font_size": font_size, "line_spacing": line_spacing, "precision": precision, "newline_count": newline_count, "newline_offset": newline_offset }