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())
|