This commit is contained in:
kim
2026-04-29 22:23:08 +09:00
parent 10eb7405bc
commit d89d7213f6
92 changed files with 380 additions and 173 deletions
+62 -20
View File
@@ -96,6 +96,12 @@ DEFAULT_FONT_SIZES = {
SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)
# Layout constants for two-column slides (text + image)
L_MARGIN = Inches(0.6) # left margin (aligns with title)
R_MARGIN = Inches(0.3) # right margin
COL_GAP = Inches(0.3) # gap between text and image columns
CONTENT_W = SLIDE_W - L_MARGIN - R_MARGIN # ~12.43"
# Dark skin names — slides with these backgrounds use light text
DARK_SKINS = {
'Cave', 'Deep Sea', 'Dream', 'Galaxy', 'Imagination', 'Metal', 'Space', 'Universe',
@@ -221,13 +227,12 @@ def is_dark_skin(name):
# ─── Helpers ────────────────────────────────────────────────────────────────────
def slugify(text: str) -> str:
"""Convert a title to a safe folder name."""
"""Convert a title to a safe folder name (matches TypeScript projectSlug logic)."""
s = str(text or "presentation").strip()
s = re.sub(r"\.pptx$", "", s, flags=re.IGNORECASE)
s = re.sub(r'[\(\)<>:"/\\|?*]+', "_", s)
s = re.sub(r"\s+", "_", s)
s = re.sub(r'[^a-zA-Z0-9ㄱ-힣_\-]', "_", s)
s = re.sub(r"_+", "_", s)
s = s.strip("_")
s = s.strip("_").lower()
return s[:60] or "presentation"
@@ -327,6 +332,9 @@ def fit_dimensions(img_w, img_h, max_w, max_h, mode="contain"):
mode='contain': fit entirely within max_w × max_h (default, no cropping)
mode='cover': fill max_w × max_h completely, cropping if necessary
"""
# Leave a small breathing margin inside the bounding box
max_w = int(max_w * 0.95)
max_h = int(max_h * 0.95)
if img_w <= 0 or img_h <= 0:
return max_w, max_h
ratio = img_w / img_h
@@ -471,6 +479,33 @@ def resolve_image_path(image_path: str, project_dir: str, workspace_path: str) -
return candidates[0] # return first even if missing (placeholder will show)
# ─── Auto Layout Helpers ────────────────────────────────────────────────────────
def _text_weight(slide_spec: dict):
"""Return (bullet_count, total_chars) for the text content of a slide."""
bullets = slide_spec.get("bullets") or slide_spec.get("bullet_points") or []
body = slide_spec.get("body") or slide_spec.get("content") or ""
if bullets:
return len(bullets), sum(len(str(b)) for b in bullets)
return (1 if body else 0), len(body)
def _auto_text_ratio(slide_spec: dict) -> float:
"""Compute text column share (0–1) based on content volume.
≤2 bullets / ≤120 chars → 0.38 image-dominant
3–4 bullets / ≤280 chars → 0.50 balanced
5+ bullets / >280 chars → 0.62 text-dominant
"""
n_bullets, n_chars = _text_weight(slide_spec)
if n_bullets <= 2 and n_chars <= 120:
return 0.38
elif n_bullets <= 4 and n_chars <= 280:
return 0.50
else:
return 0.62
# ─── Slide Generators ──────────────────────────────────────────────────────────
def make_title_slide(prs, slide_spec, spec, colors, is_dark, fs, bg_image=None):
@@ -520,42 +555,49 @@ def make_content_slide(prs, slide_spec, project_dir, workspace_path, colors, is_
set_slide_bg(slide, colors["background"])
has_title = bool(slide_spec.get("title"))
has_image = bool(slide_spec.get("image_path"))
content_y = Inches(1.5) if has_title else Inches(0.5)
content_h = SLIDE_H - content_y - Inches(0.3)
if has_title:
add_text_box(slide, Inches(0.6), Inches(0.4), Inches(12.1), Inches(0.8),
add_text_box(slide, L_MARGIN, Inches(0.4), SLIDE_W - L_MARGIN - R_MARGIN, Inches(0.8),
slide_spec["title"], font_size=Pt(fs["slide_title"]),
color_hex=colors["title"], bold=True)
# Accent underline
add_shape_rect(slide, Inches(0.6), Inches(1.15), Inches(2), Inches(0.04),
add_shape_rect(slide, L_MARGIN, Inches(1.15), Inches(2), Inches(0.04),
colors["accent"])
# Get bullet items (check aliases)
# ── Two-column layout when image present ──────────────────────────────────
if has_image:
text_ratio = _auto_text_ratio(slide_spec)
text_col_w = CONTENT_W * text_ratio - COL_GAP / 2
img_col_x = L_MARGIN + text_col_w + COL_GAP
img_col_w = CONTENT_W * (1 - text_ratio) - COL_GAP / 2
else:
text_col_w = CONTENT_W
img_col_x = None
img_col_w = None
# ── Text content ──────────────────────────────────────────────────────────
bullet_items = slide_spec.get("bullets") or slide_spec.get("bullet_points")
body_text = slide_spec.get("body") or slide_spec.get("content")
# Per-slide font_size override
override = slide_spec.get("font_size")
body_fs = Pt(override) if override else Pt(fs["body"])
body_fs = Pt(override) if override else Pt(fs["body"])
bullet_fs = Pt(override) if override else Pt(fs["bullets"])
if bullet_items and len(bullet_items) > 0:
add_bullet_list(slide, Inches(0.8), content_y, Inches(11.7), Inches(5),
add_bullet_list(slide, L_MARGIN, content_y, text_col_w, content_h,
bullet_items, font_size=bullet_fs,
color_hex=colors["body"],
bullet_color_hex=colors["accent"])
elif body_text:
add_text_box(slide, Inches(0.8), content_y, Inches(11.7), Inches(5),
add_text_box(slide, L_MARGIN, content_y, text_col_w, content_h,
body_text, font_size=body_fs, color_hex=colors["body"])
# Image on content slide (right side)
image_path = slide_spec.get("image_path")
if image_path:
resolved = resolve_image_path(image_path, project_dir, workspace_path)
img_y = Inches(1.5) if has_title else Inches(0.5)
img_max_h = Inches(5) if has_title else Inches(6)
# ── Image column ──────────────────────────────────────────────────────────
if has_image:
resolved = resolve_image_path(slide_spec["image_path"], project_dir, workspace_path)
add_image_or_placeholder(slide, resolved,
Inches(7), img_y, Inches(5.5), img_max_h,
img_col_x, content_y, img_col_w, content_h,
warnings, color_hex=colors["body"])
return slide