fix(skills): preserve EXIF orientation in generated presentations (#7193)

This commit is contained in:
ChengFa 2026-10-25 20:47:39 +08:00 committed by GitHub
parent bd62f96732
commit 5b7daf0e1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 84 additions and 1 deletions

View File

@ -1578,6 +1578,10 @@ 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 applies photo EXIF orientation before fitting and
embedding slide images, so rotated or mirrored photos retain their displayed
orientation.
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

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:
Photos with EXIF orientation metadata are rotated or mirrored before sizing and embedding, preserving their displayed orientation in the presentation.
Transparent slide images retain their alpha channel in the presentation. Opaque images are embedded as JPEG as before.
```bash

View File

@ -2,7 +2,7 @@ import json
import os
from io import BytesIO
from PIL import Image
from PIL import Image, ImageOps
from pptx import Presentation
from pptx.util import Inches
@ -60,6 +60,7 @@ def generate_ppt(
# Load and process image
with Image.open(image_path) as img:
img = ImageOps.exif_transpose(img)
image_format = "JPEG"
if "A" in img.getbands() or "transparency" in img.info:
img = img.convert("RGBA")

View File

@ -0,0 +1,76 @@
import json
import runpy
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, ImageDraw
from pptx import Presentation
SCRIPT = (
Path(__file__).resolve().parents[2]
/ "skills/public/ppt-generation/scripts/generate.py"
)
generate_ppt = runpy.run_path(str(SCRIPT))["generate_ppt"]
COLORS = {"R": (240, 0, 0), "G": (0, 220, 0), "B": (0, 0, 240), "Y": (240, 220, 0)}
@pytest.mark.parametrize("aspect_ratio", ["16:9", "4:3"])
@pytest.mark.parametrize(
("orientation", "corners"),
[
(0, "RGBY"),
(1, "RGBY"),
(2, "GRYB"),
(3, "YBGR"),
(4, "BYRG"),
(5, "RBGY"),
(6, "BRYG"),
(7, "YGBR"),
(8, "GYRB"),
],
)
def test_ppt_preserves_display_orientation(
tmp_path, aspect_ratio, orientation, corners
):
image_path = tmp_path / "photo.jpg"
with Image.new("RGB", (96, 64)) as image:
draw = ImageDraw.Draw(image)
for box, color in zip(
[(0, 0, 47, 31), (48, 0, 95, 31), (0, 32, 47, 63), (48, 32, 95, 63)],
"RGBY",
strict=True,
):
draw.rectangle(box, fill=COLORS[color])
exif = image.getexif()
if orientation:
exif[274] = orientation
image.save(image_path, quality=100, subsampling=0, exif=exif)
plan_path = tmp_path / "plan.json"
plan_path.write_text(
json.dumps({"aspect_ratio": aspect_ratio, "slides": [{"title": "Photo"}]}),
encoding="utf-8",
)
output_path = tmp_path / "presentation.pptx"
generate_ppt(str(plan_path), [str(image_path)], str(output_path))
presentation = Presentation(output_path)
picture = presentation.slides[0].shapes[0]
expected_size = (64, 96) if orientation >= 5 else (96, 64)
with Image.open(BytesIO(picture.image.blob)) as embedded:
assert embedded.size == expected_size
assert 274 not in embedded.getexif()
width, height = embedded.size
points = [
(width // 4, height // 4),
(3 * width // 4, height // 4),
(width // 4, 3 * height // 4),
(3 * width // 4, 3 * height // 4),
]
for point, color in zip(points, corners, strict=True):
assert embedded.getpixel(point) == pytest.approx(COLORS[color], abs=10)
assert picture.width / picture.height == pytest.approx(
expected_size[0] / expected_size[1], rel=1e-5
)
assert "Title: Photo" in presentation.slides[0].notes_slide.notes_text_frame.text