Files
pp-qa-km/tools/convert/from_pptx.py
2026-07-14 20:51:17 +08:00

75 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""PowerPoint (.pptx) → Markdown。每張投影片一節含表格、圖片佔位與講者備註。
可獨立執行python tools/convert/from_pptx.py <file.pptx> [-o out.md] [--dry-run]
"""
import argparse
import pathlib
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
def _table_md(tbl):
rows = [
[c.text.strip().replace("|", "\\|").replace("\n", " ") for c in row.cells]
for row in tbl.rows
]
if not rows:
return ""
width = len(rows[0])
out = ["| " + " | ".join(rows[0]) + " |", "|" + " --- |" * width]
out += ["| " + " | ".join(r) + " |" for r in rows[1:]]
return "\n".join(out)
def convert(src, assets_dir=None, md_dir=None):
"""回傳 (markdown, status)。status 恆為 'ok'"""
src = pathlib.Path(src)
md_dir = pathlib.Path(md_dir) if md_dir else src.parent
prs = Presentation(str(src))
counter = 0
sections = []
for i, slide in enumerate(prs.slides, 1):
blocks = [f"## Slide {i}"]
for shape in slide.shapes:
if shape.has_text_frame and shape.text_frame.text.strip():
blocks.append(shape.text_frame.text.strip())
elif shape.has_table:
blocks.append(_table_md(shape.table))
elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
counter += 1
name = f"img{counter}.{shape.image.ext}"
if assets_dir is not None:
assets_dir.mkdir(parents=True, exist_ok=True)
(assets_dir / name).write_bytes(shape.image.blob)
rel = (assets_dir / name).relative_to(md_dir).as_posix()
else:
rel = name
blocks.append(f"![{name}]({rel})")
if slide.has_notes_slide:
notes = slide.notes_slide.notes_text_frame.text.strip()
if notes:
blocks.append(f"### 講者備註\n\n{notes}")
sections.append("\n\n".join(blocks))
return "\n\n".join(sections), "ok"
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("src")
ap.add_argument("-o", "--output")
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args(argv)
if a.dry_run:
md, status = convert(a.src)
print(f"[dry-run] {a.src}: status={status}, {len(md)} chars")
return
out = pathlib.Path(a.output) if a.output else pathlib.Path(a.src).with_suffix(".md")
md, _ = convert(a.src, assets_dir=out.parent / f"{out.stem}-assets", md_dir=out.parent)
out.write_text(md, encoding="utf-8")
print(out)
if __name__ == "__main__":
main()