fix(skills): preserve transparency in composed PowerPoint slides (#7312)

* fix(skills): preserve transparency in composed PowerPoint slides

* ci(skills): run public skill regressions

* ci: remove public skill test workflow

* docs: move skill test commands out of inherited guidance

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
sxl-lucky7 2026-10-24 18:05:55 +08:00 committed by GitHub
parent 5dca9ae240
commit 9b89c7569d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 98 additions and 4 deletions

View File

@ -338,6 +338,10 @@ before review.
## Testing
```bash
# Public skill scripts (from the repository root)
python -m pip install -r tests/skills/requirements.txt
python -m pytest tests/skills -q
# Default backend tests (four parallel shards; excludes live and blocking-I/O tests)
cd backend
make test

View File

@ -1551,6 +1551,8 @@ view. Built-in and user-created/imported skills are listed separately. The
Community tab supports importing `.skill` archives into My skills.
General preferences remain in Settings.
The **ppt-generation** skill preserves transparent slide images when composing a presentation, including grayscale and palette PNG transparency.
Administrators can see **Could not load** warnings in Capability Center > Skills
when their own custom skill's `SKILL.md` contains invalid YAML. Warnings show the
package-relative file and, when available, its line and column, with a quoting

View File

@ -146,6 +146,8 @@ python /mnt/skills/public/image-generation/scripts/generate.py \
After all slide images are generated, call the composition script:
Transparent slide images retain their alpha channel in the presentation. Opaque images are embedded as JPEG as before.
```bash
python /mnt/skills/public/ppt-generation/scripts/generate.py \
--plan-file /mnt/user-data/workspace/presentation-plan.json \

View File

@ -60,8 +60,11 @@ def generate_ppt(
# Load and process image
with Image.open(image_path) as img:
# Convert to RGB if necessary (for PNG with transparency)
if img.mode in ("RGBA", "P"):
image_format = "JPEG"
if "A" in img.getbands() or "transparency" in img.info:
img = img.convert("RGBA")
image_format = "PNG"
elif img.mode == "P":
img = img.convert("RGB")
# Calculate dimensions to fill slide while maintaining aspect ratio
@ -88,12 +91,16 @@ def generate_ppt(
# Save processed image to bytes
img_bytes = BytesIO()
img.save(img_bytes, format="JPEG", quality=95)
img.save(img_bytes, format=image_format, quality=95)
img_bytes.seek(0)
# Add image to slide
slide.shapes.add_picture(
img_bytes, left, top, Inches(new_width_emu / 914400), Inches(new_height_emu / 914400)
img_bytes,
left,
top,
Inches(new_width_emu / 914400),
Inches(new_height_emu / 914400),
)
# Add speaker notes if available in plan

View File

@ -0,0 +1,7 @@
# Standalone public skill tests; no backend or model credentials required.
duckdb==2.0.0
openpyxl==3.1.5
Pillow==12.3.0
pytest==9.1.1
python-pptx==1.0.2
requests==2.34.2

View File

@ -0,0 +1,72 @@
"""Slide composition preserves transparency in the embedded image."""
import json
import runpy
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image
from pptx import Presentation
SCRIPT = (
Path(__file__).resolve().parents[2]
/ "skills/public/ppt-generation/scripts/generate.py"
)
@pytest.mark.parametrize("mode", ["RGBA", "LA", "P", "RGB", "L"])
def test_ppt_preserves_png_transparency(tmp_path, mode):
source = tmp_path / "slide.png"
if mode == "P":
image = Image.new("P", (64, 64), 0)
image.putpalette([0, 0, 0, 240, 80, 40] + [0] * 762)
image.putpixel((48, 32), 1)
image.save(source, transparency=0)
elif mode in {"RGB", "L"}:
image = Image.new(mode, (64, 64), 0)
image.putpixel((48, 32), (240, 80, 40) if mode == "RGB" else 160)
image.save(source, transparency=(0, 0, 0) if mode == "RGB" else 0)
else:
image = Image.new(mode, (64, 64), (0, 0, 0, 0) if mode == "RGBA" else (0, 0))
image.putpixel((48, 32), (240, 80, 40, 128) if mode == "RGBA" else (160, 128))
image.save(source)
image.close()
with Image.open(source) as original:
expected = original.convert("RGBA")
plan = tmp_path / "plan.json"
plan.write_text(
json.dumps({"slides": [{"title": "Transparent slide"}]}), encoding="utf-8"
)
output = tmp_path / "slides.pptx"
generate_ppt = runpy.run_path(str(SCRIPT))["generate_ppt"]
generate_ppt(str(plan), [str(source)], str(output))
presentation = Presentation(output)
picture = presentation.slides[0].shapes[0]
with Image.open(BytesIO(picture.image.blob)) as embedded:
assert embedded.format == "PNG"
assert embedded.convert("RGBA").tobytes() == expected.tobytes()
assert (
"Title: Transparent slide"
in presentation.slides[0].notes_slide.notes_text_frame.text
)
@pytest.mark.parametrize("mode", ["RGB", "L", "P"])
def test_ppt_keeps_opaque_images_as_jpeg(tmp_path, mode):
source = tmp_path / "slide.png"
with Image.new(mode, (64, 64), 100) as image:
image.save(source)
plan = tmp_path / "plan.json"
plan.write_text("{}", encoding="utf-8")
output = tmp_path / "slides.pptx"
runpy.run_path(str(SCRIPT))["generate_ppt"](str(plan), [str(source)], str(output))
picture = Presentation(output).slides[0].shapes[0]
with Image.open(BytesIO(picture.image.blob)) as embedded:
assert embedded.format == "JPEG"
assert embedded.size == (64, 64)