Files
codex-free-market/plugins/cjysa-presentations/skills/build-presentations/scripts/pptx_tool.py
2026-07-23 12:15:04 +08:00

100 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Original PPTX helper for structural inspection and common QA warnings."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.util import Pt
def shape_text(shape) -> str:
if not getattr(shape, "has_text_frame", False):
return ""
return "\n".join(paragraph.text for paragraph in shape.text_frame.paragraphs).strip()
def inspect(path: Path) -> None:
deck = Presentation(path)
slides = []
for index, slide in enumerate(deck.slides, start=1):
texts = [shape_text(shape) for shape in slide.shapes]
nonempty_texts = [text for text in texts if text]
slides.append(
{
"slide": index,
"shapes": len(slide.shapes),
"text_shapes": len(nonempty_texts),
"images": sum(1 for shape in slide.shapes if shape.shape_type == MSO_SHAPE_TYPE.PICTURE),
"title": (slide.shapes.title.text.strip() if slide.shapes.title and slide.shapes.title.text else None),
"text_preview": " | ".join(nonempty_texts)[:300],
}
)
report = {
"file": str(path.resolve()),
"slides": len(deck.slides),
"width_inches": round(deck.slide_width / 914400, 3),
"height_inches": round(deck.slide_height / 914400, 3),
"slide_details": slides,
}
print(json.dumps(report, ensure_ascii=False, indent=2))
def validate(path: Path, minimum_font_pt: float) -> None:
deck = Presentation(path)
warnings = []
for slide_index, slide in enumerate(deck.slides, start=1):
meaningful = 0
for shape_index, shape in enumerate(slide.shapes, start=1):
text = shape_text(shape)
if text or shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
meaningful += 1
if shape.left < 0 or shape.top < 0 or shape.left + shape.width > deck.slide_width or shape.top + shape.height > deck.slide_height:
warnings.append({"slide": slide_index, "shape": shape_index, "kind": "outside_slide"})
if getattr(shape, "has_text_frame", False):
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
if run.font.size and run.font.size < Pt(minimum_font_pt):
warnings.append(
{
"slide": slide_index,
"shape": shape_index,
"kind": "small_font",
"font_pt": round(run.font.size.pt, 1),
"text": run.text[:80],
}
)
if meaningful == 0:
warnings.append({"slide": slide_index, "kind": "blank_slide"})
title = slide.shapes.title
if title and title.has_text_frame and "\n" in title.text.strip():
warnings.append({"slide": slide_index, "kind": "multiline_title", "text": title.text.strip()[:160]})
print(json.dumps({"file": str(path.resolve()), "warnings": warnings, "count": len(warnings)}, ensure_ascii=False, indent=2))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
inspect_cmd = sub.add_parser("inspect")
inspect_cmd.add_argument("input", type=Path)
validate_cmd = sub.add_parser("validate")
validate_cmd.add_argument("input", type=Path)
validate_cmd.add_argument("--minimum-font-pt", type=float, default=14.0)
return parser
def main() -> None:
args = build_parser().parse_args()
if args.command == "inspect":
inspect(args.input)
elif args.command == "validate":
validate(args.input, args.minimum_font_pt)
if __name__ == "__main__":
main()