PPTX: ensure [Content_Types].xml has defaults for every media extension
Symptom: PowerPoint prompted to repair some generated decks. Cause: when python-pptx (or our native renderer) repacks the .pptx after we embed Oracle template assets — some of which are SVG-backed — [Content_Types].xml can lose the Default entry for "svg" (and other extensions present under ppt/media), which makes Office refuse to open the file cleanly. Fix: after writing the deck (and after any native renderer pass), walk ppt/media, collect the actual extensions in use, and ensure [Content_Types].xml declares a Default for each one (svg, png, jpg, jpeg, gif, bmp, tiff, emf, wmf, ico, webp). Repack only when at least one new Default was added — no-op fast path otherwise. Lines up with the existing field rule: every embedded media extension must be declared in [Content_Types].xml Defaults, or PowerPoint flags the file as corrupt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,9 @@ import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
@@ -848,11 +851,81 @@ class OCIDeckGenerator(OraclePresBase):
|
||||
renderer = NativePPTXDiagramRenderer()
|
||||
renderer.apply_jobs(filepath, self._native_pptx_jobs)
|
||||
|
||||
# Some Oracle template assets are SVG-backed. When python-pptx or the
|
||||
# native renderer repacks the deck, [Content_Types].xml can lose the
|
||||
# Default entry for "svg", which makes PowerPoint prompt to repair.
|
||||
self._repair_pptx_media_content_types(filepath)
|
||||
|
||||
report: dict = {"path": filepath}
|
||||
if validate:
|
||||
report["pptx"] = self._validate_pptx(filepath)
|
||||
return report
|
||||
|
||||
@staticmethod
|
||||
def _repair_pptx_media_content_types(filepath: str) -> dict:
|
||||
"""Ensure [Content_Types].xml declares every extension used in ppt/media."""
|
||||
content_type_by_ext = {
|
||||
"svg": "image/svg+xml",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"gif": "image/gif",
|
||||
"bmp": "image/bmp",
|
||||
"tiff": "image/tiff",
|
||||
"tif": "image/tiff",
|
||||
"emf": "image/x-emf",
|
||||
"wmf": "image/x-wmf",
|
||||
"ico": "image/x-icon",
|
||||
"webp": "image/webp",
|
||||
}
|
||||
|
||||
deck_path = Path(filepath)
|
||||
with tempfile.TemporaryDirectory(prefix="oci-deck-ct-") as tmp_dir:
|
||||
work_dir = Path(tmp_dir) / "deck"
|
||||
with zipfile.ZipFile(deck_path) as archive:
|
||||
archive.extractall(work_dir)
|
||||
|
||||
ct_path = work_dir / "[Content_Types].xml"
|
||||
media_dir = work_dir / "ppt" / "media"
|
||||
if not ct_path.exists() or not media_dir.exists():
|
||||
return {"status": "skipped", "added": []}
|
||||
|
||||
tree = ET.parse(ct_path)
|
||||
root = tree.getroot()
|
||||
ns = "http://schemas.openxmlformats.org/package/2006/content-types"
|
||||
existing = {
|
||||
(node.get("Extension") or "").lower()
|
||||
for node in root.findall(f"{{{ns}}}Default")
|
||||
}
|
||||
|
||||
added: list[str] = []
|
||||
for media_path in sorted(media_dir.iterdir()):
|
||||
if not media_path.is_file():
|
||||
continue
|
||||
ext = media_path.suffix.lstrip(".").lower()
|
||||
content_type = content_type_by_ext.get(ext)
|
||||
if not content_type or ext in existing:
|
||||
continue
|
||||
default = ET.SubElement(root, f"{{{ns}}}Default")
|
||||
default.set("Extension", ext)
|
||||
default.set("ContentType", content_type)
|
||||
existing.add(ext)
|
||||
added.append(ext)
|
||||
|
||||
if not added:
|
||||
return {"status": "pass", "added": []}
|
||||
|
||||
tree.write(ct_path, encoding="UTF-8", xml_declaration=True)
|
||||
temp_output = deck_path.with_suffix(".tmp.pptx")
|
||||
if temp_output.exists():
|
||||
temp_output.unlink()
|
||||
with zipfile.ZipFile(temp_output, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for path in sorted(work_dir.rglob("*")):
|
||||
if path.is_file():
|
||||
archive.write(path, path.relative_to(work_dir))
|
||||
temp_output.replace(deck_path)
|
||||
return {"status": "fixed", "added": sorted(added)}
|
||||
|
||||
@staticmethod
|
||||
def _validate_pptx(filepath: str) -> dict:
|
||||
"""Pre-delivery sanity check on the generated .pptx.
|
||||
|
||||
Reference in New Issue
Block a user