Release 2.8.2: render title-slide photo background in PPTX
The slide editor draws a title slide's image as a full-bleed darkened background, but pptx_gen.py's make_title_slide ignored image_path entirely and only drew the skin background — so photo backgrounds showed in the editor but vanished from the generated PPTX / preview. - make_title_slide now renders image_path full-bleed (cover) with a dark overlay and white title/subtitle, matching the editor preview. - Add set_shape_fill_opacity() that injects OOXML <a:alpha>, since python-pptx's fill.transparency is a silent no-op (the overlay was rendering as fully opaque black). Apply it to the fullscreen bottom bar too. - Pass project_dir/workspace_path/warnings into make_title_slide. - Bump version to 2.8.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "smallclaw",
|
||||
"version": "2.8.1",
|
||||
"version": "2.8.2",
|
||||
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
+49
-11
@@ -506,6 +506,26 @@ def add_shape_rect(slide, left, top, width, height, color_hex: str):
|
||||
return shape
|
||||
|
||||
|
||||
def set_shape_fill_opacity(shape, opacity_pct: float):
|
||||
"""Make a solid-filled shape semi-transparent.
|
||||
|
||||
opacity_pct: 0 (fully transparent) .. 100 (fully opaque).
|
||||
python-pptx has no fill.transparency setter (it is a silent no-op), so we
|
||||
inject the OOXML <a:alpha> element directly into the solidFill color.
|
||||
"""
|
||||
spPr = shape._element.spPr
|
||||
fill = spPr.find(qn('a:solidFill'))
|
||||
if fill is None:
|
||||
return
|
||||
srgb = fill.find(qn('a:srgbClr'))
|
||||
if srgb is None:
|
||||
return
|
||||
alpha = srgb.find(qn('a:alpha'))
|
||||
if alpha is None:
|
||||
alpha = etree.SubElement(srgb, qn('a:alpha'))
|
||||
alpha.set('val', str(int(max(0, min(100, opacity_pct)) * 1000)))
|
||||
|
||||
|
||||
def fit_dimensions(img_w, img_h, max_w, max_h, mode="contain"):
|
||||
"""Calculate dimensions preserving aspect ratio.
|
||||
|
||||
@@ -697,12 +717,31 @@ def _auto_text_ratio(slide_spec: dict) -> float:
|
||||
|
||||
# ─── Slide Generators ──────────────────────────────────────────────────────────
|
||||
|
||||
def make_title_slide(prs, slide_spec, spec, colors, is_dark, fs, bg_image=None):
|
||||
def make_title_slide(prs, slide_spec, spec, colors, is_dark, fs, bg_image=None,
|
||||
project_dir=None, workspace_path=None, warnings=None):
|
||||
"""Generate a title slide."""
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank layout
|
||||
|
||||
# Background
|
||||
if bg_image:
|
||||
title_col = colors["title"]
|
||||
subtitle_col = colors["subtitle"]
|
||||
|
||||
# Background — a photo (slide image_path) takes precedence over the skin,
|
||||
# rendered full-bleed with a dark overlay + white text. This mirrors the
|
||||
# editor preview, which draws the title image as a darkened background.
|
||||
img_path = slide_spec.get("image_path")
|
||||
has_photo = bool(img_path) and project_dir is not None
|
||||
if has_photo:
|
||||
resolved = resolve_image_path(img_path, project_dir, workspace_path)
|
||||
add_image_or_placeholder(slide, resolved,
|
||||
Inches(0), Inches(0), SLIDE_W, SLIDE_H,
|
||||
warnings if warnings is not None else [],
|
||||
fit_mode="cover")
|
||||
# Full-slide dark overlay (~48% opaque black, matches editor rgba(0,0,0,.48))
|
||||
overlay = add_shape_rect(slide, Inches(0), Inches(0), SLIDE_W, SLIDE_H, "000000")
|
||||
set_shape_fill_opacity(overlay, 48) # 48% opaque black, matches editor rgba(0,0,0,.48)
|
||||
title_col = "FFFFFF"
|
||||
subtitle_col = "E6E6E6"
|
||||
elif bg_image:
|
||||
set_slide_bg_image(slide, bg_image)
|
||||
elif is_dark:
|
||||
set_slide_bg(slide, colors["background"])
|
||||
@@ -710,17 +749,17 @@ def make_title_slide(prs, slide_spec, spec, colors, is_dark, fs, bg_image=None):
|
||||
_title_font = slide_spec.get("_font_override") or None
|
||||
title = slide_spec.get("title") or spec.get("title") or "Untitled"
|
||||
title_pt = slide_spec.get("title_size") or fs["title"]
|
||||
y_pos = Inches(2.8) if bg_image else Inches(2.5)
|
||||
y_pos = Inches(2.8) if (bg_image or has_photo) else Inches(2.5)
|
||||
title_lines = len(title.split('\n'))
|
||||
title_h = Inches(max(1.2, title_lines * title_pt * 1.5 / 72))
|
||||
add_text_box(slide, Inches(0.8), y_pos, Inches(11.7), title_h,
|
||||
title, font_size=Pt(title_pt), color_hex=colors["title"],
|
||||
title, font_size=Pt(title_pt), color_hex=title_col,
|
||||
bold=True, alignment=PP_ALIGN.CENTER, font_name=_title_font)
|
||||
|
||||
subtitle = slide_spec.get("subtitle")
|
||||
if subtitle:
|
||||
add_text_box(slide, Inches(1.5), y_pos + title_h + Inches(0.15), Inches(10.3), Inches(0.7),
|
||||
subtitle, font_size=Pt(fs["subtitle"]), color_hex=colors["subtitle"],
|
||||
subtitle, font_size=Pt(fs["subtitle"]), color_hex=subtitle_col,
|
||||
alignment=PP_ALIGN.CENTER, font_name=_title_font)
|
||||
return slide
|
||||
|
||||
@@ -779,10 +818,8 @@ def make_content_slide(prs, slide_spec, project_dir, workspace_path, colors, is_
|
||||
# Semi-transparent dark bar at bottom for text legibility
|
||||
bar_h = Inches(2.0)
|
||||
bar_y = SLIDE_H - bar_h
|
||||
add_shape_rect(slide, Inches(0), bar_y, SLIDE_W, bar_h, "000000")
|
||||
bar_shape = slide.shapes[-1]
|
||||
bar_shape.fill.fore_color.rgb = hex_to_rgb("000000")
|
||||
bar_shape.fill.transparency = 0.45
|
||||
bar_shape = add_shape_rect(slide, Inches(0), bar_y, SLIDE_W, bar_h, "000000")
|
||||
set_shape_fill_opacity(bar_shape, 55) # 55% opaque black
|
||||
# Title and body overlaid on dark bar
|
||||
fs_title_pt = slide_spec.get("title_size") or fs["slide_title"]
|
||||
if has_title:
|
||||
@@ -1495,7 +1532,8 @@ def generate(spec: dict, workspace_path: str) -> dict:
|
||||
if slide_type == "section":
|
||||
slide = make_section_slide(prs, slide_spec, slide_colors, fs, bg_image)
|
||||
elif slide_type == "title":
|
||||
slide = make_title_slide(prs, slide_spec, spec, slide_colors, slide_is_dark, fs, bg_image)
|
||||
slide = make_title_slide(prs, slide_spec, spec, slide_colors, slide_is_dark, fs, bg_image,
|
||||
project_dir, workspace_path, warnings)
|
||||
elif slide_type == "image":
|
||||
slide = make_image_slide(prs, slide_spec, project_dir, workspace_path,
|
||||
slide_colors, warnings, slide_is_dark, fs, bg_image)
|
||||
|
||||
Reference in New Issue
Block a user