v2.0
This commit is contained in:
+62
-20
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user