75 lines
2.6 KiB
Python
75 lines
2.6 KiB
Python
"""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"")
|
||
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()
|