mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-07 02:03:55 -05:00
- Wire up slides-preview-dev.yml to deploy via SSH (matching labs/kits pattern) - Add slides/ URL rewrite in book-preview-dev.yml for landing page links - Add pdf2pptx.py conversion script (pdftoppm + python-pptx, 300 DPI) - Update Makefile with pptx targets (per-chapter, per-volume, all) - Update slides-build-pdfs.yml and slides-publish-live.yml to build PPTX and include PDF+PPTX ZIP archives in GitHub releases - Fix all 44 overfull box warnings across 17 Vol1 Beamer decks - Update slides portal pages with direct per-deck PDF/PPTX download links, PPTX column in tables, and remove placeholder callouts - Update teaching.qmd with PPTX build instructions - Cross-link slides from instructors hub, getting-started, and both syllabi
125 lines
4.2 KiB
Python
Executable File
125 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Convert PDF slide decks to PowerPoint (PPTX) using high-resolution images.
|
|
|
|
Each PDF page is rendered at 300 DPI via pdftoppm (poppler) and placed as a
|
|
full-bleed image on a 16:9 PowerPoint slide. The result is visually identical
|
|
to the PDF — suitable for presenting in PowerPoint/Keynote with annotations,
|
|
presenter view, etc. Slides are image-based (not editable text).
|
|
|
|
Requirements:
|
|
- pdftoppm (poppler-utils): apt install poppler-utils / brew install poppler
|
|
- python-pptx: pip install python-pptx
|
|
- Pillow: pip install Pillow
|
|
|
|
Usage:
|
|
python3 pdf2pptx.py input.pdf # -> input.pptx
|
|
python3 pdf2pptx.py input.pdf -o output.pptx # explicit output
|
|
python3 pdf2pptx.py _build/vol1/*.pdf # batch convert
|
|
python3 pdf2pptx.py _build/vol1/*.pdf --dpi 200 # lower DPI (smaller files)
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
from pathlib import Path
|
|
|
|
def check_dependencies():
|
|
if not shutil.which("pdftoppm"):
|
|
print("ERROR: pdftoppm not found. Install poppler:", file=sys.stderr)
|
|
print(" macOS: brew install poppler", file=sys.stderr)
|
|
print(" Ubuntu: sudo apt install poppler-utils", file=sys.stderr)
|
|
sys.exit(1)
|
|
try:
|
|
from pptx import Presentation # noqa: F401
|
|
except ImportError:
|
|
print("ERROR: python-pptx not found. Install:", file=sys.stderr)
|
|
print(" pip install python-pptx Pillow", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def convert_pdf_to_pptx(pdf_path: str, pptx_path: str, dpi: int = 300) -> dict:
|
|
"""Convert a single PDF to PPTX. Returns stats dict."""
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Emu
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
subprocess.run(
|
|
["pdftoppm", "-png", "-r", str(dpi), pdf_path, os.path.join(tmp, "slide")],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
images = sorted(glob.glob(os.path.join(tmp, "slide-*.png")))
|
|
if not images:
|
|
return {"ok": False, "error": "pdftoppm produced no images"}
|
|
|
|
prs = Presentation()
|
|
prs.slide_width = Inches(13.333)
|
|
prs.slide_height = Inches(7.5)
|
|
|
|
for img_path in images:
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
|
slide.shapes.add_picture(
|
|
img_path, Emu(0), Emu(0), prs.slide_width, prs.slide_height
|
|
)
|
|
|
|
os.makedirs(os.path.dirname(pptx_path) or ".", exist_ok=True)
|
|
prs.save(pptx_path)
|
|
|
|
size_mb = os.path.getsize(pptx_path) / (1024 * 1024)
|
|
return {"ok": True, "slides": len(images), "size_mb": size_mb}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert PDF slide decks to PowerPoint (image-based)"
|
|
)
|
|
parser.add_argument("pdfs", nargs="+", help="PDF file(s) to convert")
|
|
parser.add_argument("-o", "--output", help="Output path (single file only)")
|
|
parser.add_argument("--dpi", type=int, default=300, help="Render DPI (default: 300)")
|
|
parser.add_argument("--output-dir", help="Output directory (batch mode)")
|
|
args = parser.parse_args()
|
|
|
|
check_dependencies()
|
|
|
|
if args.output and len(args.pdfs) > 1:
|
|
print("ERROR: -o/--output only works with a single input file", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
total = 0
|
|
failed = 0
|
|
|
|
for pdf in args.pdfs:
|
|
if not os.path.isfile(pdf):
|
|
print(f" SKIP {pdf} (not found)")
|
|
continue
|
|
|
|
if args.output:
|
|
pptx = args.output
|
|
elif args.output_dir:
|
|
pptx = os.path.join(args.output_dir, Path(pdf).stem + ".pptx")
|
|
else:
|
|
pptx = str(Path(pdf).with_suffix(".pptx"))
|
|
|
|
print(f" Converting {pdf}...")
|
|
result = convert_pdf_to_pptx(pdf, pptx, dpi=args.dpi)
|
|
|
|
if result["ok"]:
|
|
print(f" -> {pptx} ({result['slides']} slides, {result['size_mb']:.1f} MB)")
|
|
total += 1
|
|
else:
|
|
print(f" FAILED: {result['error']}")
|
|
failed += 1
|
|
|
|
print(f"\nDone: {total} converted, {failed} failed")
|
|
if failed:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|