Initial commit: Hermes Agent Skills collection
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Validate a .pptx file for PowerPoint compatibility.
|
||||
|
||||
Checks for issues that LibreOffice tolerates but PowerPoint rejects:
|
||||
- Negative shape dimensions (cx, cy ≤ 0) in <a:ext> elements
|
||||
- Negative shape positions (x, y < 0) in <a:off> elements
|
||||
- Missing required OOXML parts ([Content_Types].xml, presentation.xml, etc.)
|
||||
- Malformed XML in any part
|
||||
- Broken relationship references (rId in slide XML not found in .rels)
|
||||
- Missing Content_Type overrides for slides
|
||||
|
||||
Usage:
|
||||
python3 validate_pptx_compat.py <file.pptx>
|
||||
|
||||
Exit codes:
|
||||
0 = all checks passed
|
||||
1 = issues found (printed to stderr)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
NS_P = "http://schemas.openxmlformats.org/presentationml/2006/main"
|
||||
NS_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
NS_REL = "{http://schemas.openxmlformats.org/package/2006/relationships}"
|
||||
|
||||
|
||||
def validate(pptx_path: str) -> list[str]:
|
||||
"""Return a list of issue strings; empty list means clean."""
|
||||
issues = []
|
||||
|
||||
with zipfile.ZipFile(pptx_path, "r") as z:
|
||||
# 1. Zip integrity
|
||||
bad = z.testzip()
|
||||
if bad:
|
||||
issues.append(f"BAD ZIP ENTRY: {bad}")
|
||||
|
||||
# 2. XML parse check on all XML/rels parts
|
||||
for name in z.namelist():
|
||||
if name.endswith(".xml") or name.endswith(".rels"):
|
||||
try:
|
||||
ET.parse(z.open(name))
|
||||
except ET.ParseError as e:
|
||||
issues.append(f"XML PARSE ERROR in {name}: {e}")
|
||||
|
||||
# 3. Required parts
|
||||
required = [
|
||||
"[Content_Types].xml",
|
||||
"_rels/.rels",
|
||||
"ppt/presentation.xml",
|
||||
"ppt/_rels/presentation.xml.rels",
|
||||
"ppt/theme/theme1.xml",
|
||||
]
|
||||
for req in required:
|
||||
if req not in z.namelist():
|
||||
issues.append(f"MISSING REQUIRED PART: {req}")
|
||||
|
||||
# 4. Slide rels existence
|
||||
slides = [
|
||||
f for f in z.namelist()
|
||||
if f.startswith("ppt/slides/slide") and f.endswith(".xml")
|
||||
]
|
||||
for s in sorted(slides):
|
||||
rels = s.replace("ppt/slides/", "ppt/slides/_rels/") + ".rels"
|
||||
if rels not in z.namelist():
|
||||
issues.append(f"MISSING RELS for {s}")
|
||||
|
||||
# 5. Content_Type overrides for slides
|
||||
ct = ET.parse(z.open("[Content_Types].xml"))
|
||||
ct_ns = {"ct": "http://schemas.openxmlformats.org/package/2006/content-types"}
|
||||
overrides = [
|
||||
o.get("PartName") for o in ct.findall(".//ct:Override", ct_ns)
|
||||
]
|
||||
for s in sorted(slides):
|
||||
part_name = "/" + s
|
||||
if part_name not in overrides:
|
||||
issues.append(f"Missing Content_Type override for {s}")
|
||||
|
||||
# 6. Negative dimensions and positions (THE key check)
|
||||
for i in range(1, len(slides) + 1):
|
||||
slide_path = f"ppt/slides/slide{i}.xml"
|
||||
if slide_path not in z.namelist():
|
||||
continue
|
||||
root = ET.parse(z.open(slide_path)).getroot()
|
||||
|
||||
for sp in root.iter(f"{{{NS_P}}}sp"):
|
||||
spPr = sp.find(f".//{{{NS_P}}}spPr")
|
||||
if spPr is None:
|
||||
continue
|
||||
|
||||
# Get shape name for reporting
|
||||
cNvPr = sp.find(f".//{{{NS_P}}}cNvPr")
|
||||
name = cNvPr.get("name", "?") if cNvPr is not None else "?"
|
||||
|
||||
xfrm = spPr.find(f"{{{NS_A}}}xfrm")
|
||||
if xfrm is None:
|
||||
continue
|
||||
|
||||
off = xfrm.find(f"{{{NS_A}}}off")
|
||||
ext = xfrm.find(f"{{{NS_A}}}ext")
|
||||
|
||||
if ext is not None:
|
||||
cx = int(ext.get("cx", "0"))
|
||||
cy = int(ext.get("cy", "0"))
|
||||
if cx <= 0 or cy <= 0:
|
||||
issues.append(
|
||||
f"slide{i}: shape \"{name}\" has zero/negative extent: "
|
||||
f"cx={cx} cy={cy}"
|
||||
)
|
||||
|
||||
if off is not None:
|
||||
x = int(off.get("x", "0"))
|
||||
y = int(off.get("y", "0"))
|
||||
if x < 0 or y < 0:
|
||||
issues.append(
|
||||
f"slide{i}: shape \"{name}\" has negative position: "
|
||||
f"x={x} y={y}"
|
||||
)
|
||||
|
||||
# 7. Broken image embed references
|
||||
for i in range(1, len(slides) + 1):
|
||||
slide_path = f"ppt/slides/slide{i}.xml"
|
||||
if slide_path not in z.namelist():
|
||||
continue
|
||||
root = ET.parse(z.open(slide_path)).getroot()
|
||||
|
||||
rels_path = f"ppt/slides/_rels/slide{i}.xml.rels"
|
||||
if rels_path not in z.namelist():
|
||||
continue
|
||||
rels_tree = ET.parse(z.open(rels_path))
|
||||
rels_root = rels_tree.getroot()
|
||||
valid_rids = {
|
||||
rel.get("Id") for rel in rels_root.findall(f"{NS_REL}Relationship")
|
||||
}
|
||||
|
||||
for blip in root.iter(f"{{{NS_A}}}blip"):
|
||||
embed = blip.get(f"{{{NS_R}}}embed")
|
||||
if embed is not None and embed not in valid_rids:
|
||||
issues.append(
|
||||
f"slide{i}: blipFill embed '{embed}' not found in rels"
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: validate_pptx_compat.py <file.pptx>", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
pptx_path = sys.argv[1]
|
||||
try:
|
||||
issues = validate(pptx_path)
|
||||
except FileNotFoundError:
|
||||
print(f"File not found: {pptx_path}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
except zipfile.BadZipFile:
|
||||
print(f"Not a valid ZIP/PPTX file: {pptx_path}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
if issues:
|
||||
print(f"❌ {len(issues)} issue(s) found:", file=sys.stderr)
|
||||
for iss in issues:
|
||||
print(f" • {iss}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"✅ {pptx_path}: all checks passed")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user