"""Capacity test worker for slide text fit and rendering. Input JSON (stdin): { "text": str, "width": int, "height": int, "with_image": bool, "page_index": int, "metrics_only": bool } """ from __future__ import annotations import base64 import json import logging import math import os import re import sys import tempfile import traceback from pathlib import Path logging.basicConfig(level=logging.WARNING, format="%(levelname)s:%(name)s:%(message)s") _log = logging.getLogger(__name__) def _wrap_paragraph_lines(text: str, font, max_width: int) -> list[str]: words = text.split(" ") if not words: return [""] lines: list[str] = [] cur = "" for w in words: cand = (cur + " " + w).strip() if not cur or font.size(cand)[0] <= max_width: cur = cand else: lines.append(cur) # hard-break long words if font.size(w)[0] <= max_width: cur = w else: chunk = "" for ch in w: cc = chunk + ch if font.size(cc)[0] <= max_width: chunk = cc else: if chunk: lines.append(chunk) chunk = ch cur = chunk if cur: lines.append(cur) return lines def _paginate(text: str, font, max_width: int, max_lines: int) -> list[str]: paras = text.replace("\r\n", "\n").split("\n") all_lines: list[str] = [] for i, p in enumerate(paras): if p.strip(): all_lines.extend(_wrap_paragraph_lines(p, font, max_width)) else: all_lines.append("") if i != len(paras) - 1: all_lines.append("") pages = [] for i in range(0, len(all_lines), max_lines): pages.append("\n".join(all_lines[i:i + max_lines]).strip("\n")) return pages or [""] def _render(payload: dict) -> dict: os.environ.setdefault("SDL_VIDEODRIVER", "dummy") os.environ.setdefault("PYGAME_HIDE_SUPPORT_PROMPT", "1") import pygame # noqa: PLC0415 if not pygame.get_init(): pygame.init() if not pygame.font.get_init(): pygame.font.init() w = int(payload.get("width", 1300)) h = int(payload.get("height", 812)) with_image = bool(payload.get("with_image", False)) metrics_only = bool(payload.get("metrics_only", False)) text = str(payload.get("text", "") or "") req_page = max(0, int(payload.get("page_index", 0))) font_header = pygame.font.SysFont("dejavusans", max(22, int(h * 0.035)), bold=True) font_sub = pygame.font.SysFont("dejavusans", max(16, int(h * 0.022))) font_body = pygame.font.SysFont("dejavusans", max(20, int(h * 0.028))) panel = pygame.Rect(int(0.04 * w), int(0.05 * h), int(0.92 * w), int(0.9 * h)) content = pygame.Rect(panel.x + 24, panel.y + 95, panel.w - 48, panel.h - 120) text_rect = content.inflate(-28, -24) image_rect = None if with_image: usable_area = text_rect.w * text_rect.h side = int(math.sqrt(usable_area * 0.12)) # ~12% area square side = max(70, min(side, int(text_rect.h * 0.38))) image_rect = pygame.Rect(text_rect.right - side, text_rect.y + 4, side, side) # text should not collide with image: reserve right gutter globally (simple + deterministic) text_rect.width -= (side + 16) line_h = int(font_body.get_height() * 1.45) max_lines = max(1, text_rect.h // line_h) pages = _paginate(text, font_body, text_rect.w, max_lines) page_index = min(req_page, len(pages) - 1) if page_index != req_page: _log.warning("capacity worker: page_index clamped from %d to %d (total pages: %d)", req_page, page_index, len(pages)) page_text = pages[page_index] all_chars = len(page_text) non_space = len(re.sub(r"\s+", "", page_text)) overflow_chars = max(0, len(text) - len("\n".join(pages[: page_index + 1]))) if metrics_only: return { "success": True, "page_index": page_index, "total_pages": len(pages), "page_text": page_text, "all_pages_text": pages, "chars_on_page": all_chars, "usable_chars_on_page": non_space, "max_lines": int(max_lines), "line_height_px": int(line_h), "with_image": with_image, "overflow_chars": overflow_chars, } surf = pygame.Surface((w, h), pygame.SRCALPHA) # slide background surf.fill((15, 21, 48)) pygame.draw.rect(surf, (12, 17, 38), panel, border_radius=16) pygame.draw.rect(surf, (63, 79, 124), panel, width=2, border_radius=16) # header region (matches current conceptual slide structure) sub_txt = "Capacity Test ยท Slide Fit Meter" head_txt = "Text Capacity Measurement" sub_s = font_sub.render(sub_txt, True, (161, 178, 216)) head_s = font_header.render(head_txt, True, (240, 245, 255)) surf.blit(sub_s, (panel.x + 24, panel.y + 18)) surf.blit(head_s, (panel.x + 24, panel.y + 46)) # content box with hard clipping pygame.draw.rect(surf, (20, 28, 58), content, border_radius=12) pygame.draw.rect(surf, (70, 86, 130), content, width=1, border_radius=12) if with_image and image_rect is not None: pygame.draw.rect(surf, (42, 51, 87), image_rect, border_radius=8) pygame.draw.rect(surf, (113, 132, 189), image_rect, width=1, border_radius=8) # render text with hard clip old_clip = surf.get_clip() surf.set_clip(text_rect) y = text_rect.y for ln in page_text.split("\n"): if y + line_h > text_rect.bottom: break line_s = font_body.render(ln, True, (229, 236, 252)) surf.blit(line_s, (text_rect.x, y)) y += line_h surf.set_clip(old_clip) # page badge page_badge = font_sub.render(f"Page {page_index + 1} / {len(pages)}", True, (161, 178, 216)) surf.blit(page_badge, (panel.right - page_badge.get_width() - 22, panel.y + 22)) with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: path = tmp.name try: pygame.image.save(surf, path) png = Path(path).read_bytes() finally: try: Path(path).unlink(missing_ok=True) except Exception: pass return { "success": True, "data_url": "data:image/png;base64," + base64.b64encode(png).decode("ascii"), "page_index": page_index, "total_pages": len(pages), "page_text": page_text, "all_pages_text": pages, "chars_on_page": all_chars, "usable_chars_on_page": non_space, "max_lines": int(max_lines), "line_height_px": int(line_h), "with_image": with_image, "overflow_chars": overflow_chars, } def main() -> int: try: payload = json.loads(sys.stdin.read() or "{}") out = _render(payload) sys.stdout.write(json.dumps(out)) return 0 except Exception as exc: sys.stdout.write(json.dumps({"success": False, "error": str(exc), "traceback": traceback.format_exc()})) return 1 if __name__ == "__main__": raise SystemExit(main())