51570f4da7
The bundled docx, xlsx, powerpoint, and pdf skills were adapted from Anthropic's document skills and carried their proprietary LICENSE.txt (no derivatives, no redistribution). Flagged as critical license findings by the SkillEvaluator Tier 1 scan of our skill tree. This replaces all four with clean-room rewrites: - Authored from scratch against library knowledge only (python-docx, openpyxl, python-pptx, pypdf/reportlab/pdfplumber — all MIT/BSD) by isolated subagents given functional specs, with an explicit prohibition on reading the prior skill content or anthropics/skills; session transcripts retained as provenance evidence. - MIT licensed (LICENSE file per skill), author: Nous Research. - Each skill: SKILL.md to house standards + argparse helper scripts with UTF-8-explicit I/O + its own e2e pytest suite (fixtures built on the fly, non-ASCII round-trips run under LC_ALL=C). - All four pass SkillEvaluator Tier 1 pii+unicode+lint 3/3. tests/skills/test_office_document_skills.py rewritten against the new contracts: MIT/no-Anthropic-text invariants, scripts documented in SKILL.md, argparse CLI shape, and a no-locale-default-open() check (which caught and fixed a real gap: pdfplumber text reads are fine, but the invariant scan now guards every future script). Docs pages regenerated for the four skills (scoped; unrelated generator drift excluded). Honest capability deltas vs the old versions are documented per SKILL.md (e.g. tracked-changes accept/reject and OOXML XSD validation are not reimplemented; form flattening limits stated).
132 lines
4.6 KiB
Python
132 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Read a .pptx file: JSON outline, notes, or export embedded images.
|
|
|
|
Modes:
|
|
--outline JSON with per-slide layout, texts, tables, notes,
|
|
chart data, and image inventory (default mode).
|
|
--notes JSON list of speaker notes per slide.
|
|
--images DIR Export every embedded picture to DIR as files.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
from pptx import Presentation
|
|
from pptx.enum.shapes import MSO_SHAPE_TYPE
|
|
from pptx.util import Emu
|
|
|
|
|
|
def iter_shapes(shapes):
|
|
"""Yield shapes, descending into groups."""
|
|
for shape in shapes:
|
|
if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
|
|
yield from iter_shapes(shape.shapes)
|
|
else:
|
|
yield shape
|
|
|
|
|
|
def chart_info(chart):
|
|
info = {"type": str(chart.chart_type),
|
|
"categories": [str(c) for c in chart.plots[0].categories],
|
|
"series": []}
|
|
for plot in chart.plots:
|
|
for series in plot.series:
|
|
try:
|
|
name = series.name
|
|
except (AttributeError, KeyError):
|
|
name = None
|
|
info["series"].append({"name": name,
|
|
"values": list(series.values)})
|
|
return info
|
|
|
|
|
|
def slide_record(index, slide):
|
|
rec = {"index": index, "layout": slide.slide_layout.name,
|
|
"texts": [], "tables": [], "images": [], "charts": [],
|
|
"notes": None}
|
|
for shape in iter_shapes(slide.shapes):
|
|
if shape.has_text_frame and shape.text_frame.text.strip():
|
|
rec["texts"].append(shape.text_frame.text)
|
|
if shape.has_table:
|
|
rec["tables"].append(
|
|
[[cell.text for cell in row.cells]
|
|
for row in shape.table.rows])
|
|
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
|
|
try:
|
|
img = shape.image
|
|
rec["images"].append({"filename": img.filename,
|
|
"ext": img.ext,
|
|
"size_bytes": len(img.blob)})
|
|
except (KeyError, ValueError):
|
|
rec["images"].append({"filename": None, "ext": None,
|
|
"size_bytes": None,
|
|
"note": "linked or unreadable"})
|
|
if shape.has_chart:
|
|
rec["charts"].append(chart_info(shape.chart))
|
|
if slide.has_notes_slide:
|
|
rec["notes"] = slide.notes_slide.notes_text_frame.text
|
|
return rec
|
|
|
|
|
|
def export_images(prs, out_dir):
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
written = []
|
|
for i, slide in enumerate(prs.slides):
|
|
for j, shape in enumerate(iter_shapes(slide.shapes)):
|
|
if shape.shape_type != MSO_SHAPE_TYPE.PICTURE:
|
|
continue
|
|
try:
|
|
img = shape.image
|
|
except (KeyError, ValueError):
|
|
continue
|
|
path = os.path.join(out_dir, f"slide{i}_img{j}.{img.ext}")
|
|
with open(path, "wb") as fh:
|
|
fh.write(img.blob)
|
|
written.append(path)
|
|
return written
|
|
|
|
|
|
def main(argv=None):
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
parser = argparse.ArgumentParser(
|
|
description="Read a .pptx: outline/notes as JSON, export images.")
|
|
parser.add_argument("pptx", help="path to the .pptx file")
|
|
parser.add_argument("--outline", action="store_true",
|
|
help="print full JSON outline (default)")
|
|
parser.add_argument("--notes", action="store_true",
|
|
help="print speaker notes only")
|
|
parser.add_argument("--images", metavar="DIR",
|
|
help="export embedded images into DIR")
|
|
args = parser.parse_args(argv)
|
|
|
|
prs = Presentation(args.pptx)
|
|
|
|
if args.images:
|
|
written = export_images(prs, args.images)
|
|
print(json.dumps({"ok": True, "exported": written}, indent=2))
|
|
return 0
|
|
if args.notes:
|
|
notes = [slide.notes_slide.notes_text_frame.text
|
|
if slide.has_notes_slide else None
|
|
for slide in prs.slides]
|
|
print(json.dumps({"ok": True, "notes": notes},
|
|
indent=2, ensure_ascii=True))
|
|
return 0
|
|
|
|
outline = {
|
|
"ok": True,
|
|
"slide_size_inches": [round(Emu(prs.slide_width).inches, 3),
|
|
round(Emu(prs.slide_height).inches, 3)],
|
|
"slide_count": len(prs.slides._sldIdLst),
|
|
"layouts_available": [lay.name for lay in prs.slide_layouts],
|
|
"slides": [slide_record(i, s) for i, s in enumerate(prs.slides)],
|
|
}
|
|
print(json.dumps(outline, indent=2, ensure_ascii=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|