#!/usr/bin/env python3
"""
pptx-to-html: Convert a PowerPoint deck (.pptx / .ppt / .pptm) into a single,
self-contained HTML "viewer" that mirrors the deck's formatting and page
structure. Each slide is rendered to a high-resolution image (via PowerPoint
itself) and embedded as base64 so the output is one portable file.
Usage:
python convert.py --input "C:\\path\\deck.pptx" [--out "C:\\dir"] [--scale 2.0]
python convert.py --input "a.pptx" --input "b.ppt" (multiple files)
python convert.py --input "deck.pptx" --titles titles.json (override labels)
Output: ".html" written next to each source file, or into
--out if provided. Prints a JSON summary to stdout.
Requires: Windows + Microsoft PowerPoint + pywin32. No LibreOffice needed.
Handles legacy binary .ppt files (OLE2) that zip-based readers cannot open.
"""
import argparse
import base64
import datetime
import html
import json
import os
import re
import shutil
import sys
import tempfile
MSO_TRUE = -1
MSO_FALSE = 0
BLANK_PNG_BYTES = 1500 # exports smaller than this are treated as blank slides
MONTHS = ["", "January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"]
# --------------------------------------------------------------------------- #
# HTML template (tokens are %%UPPER%% to avoid clashing with CSS braces)
# --------------------------------------------------------------------------- #
TEMPLATE = r"""
%%DOCTITLE%%
%%BRAND%%
%%DATEBLOCK%%
%%N%% slides
%%SLIDES%%
"""
def clean_text(s):
if not s:
return ""
s = re.sub(r"[\r\n\x0b\x07]+", " ", s)
return re.sub(r"\s+", " ", s).strip()
def parse_name(basename):
"""Return (brand, date_label_or_None) from a file basename.
Recognizes a leading YYYY.MM.DD / YYYY-MM-DD / YYYY_MM_DD prefix.
"""
m = re.match(r"^(\d{4})[.\-_](\d{1,2})[.\-_](\d{1,2})\s+(.*)$", basename)
if m:
y, mo, d, rest = int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4).strip()
date_label = None
if 1 <= mo <= 12 and 1 <= d <= 31:
try:
datetime.date(y, mo, d)
date_label = "%s %d, %d" % (MONTHS[mo], d, y)
except ValueError:
date_label = None
return (rest or basename, date_label)
return (basename, None)
def derive_title(slide):
"""Best-effort human label for a slide."""
try:
shapes = slide.Shapes
if shapes.HasTitle:
t = clean_text(shapes.Title.TextFrame.TextRange.Text)
if t:
return t[:90]
except Exception:
pass
# Fallback: first non-empty text on the slide
try:
shapes = slide.Shapes
for i in range(1, shapes.Count + 1):
shp = shapes.Item(i)
try:
if shp.HasTextFrame and shp.TextFrame.HasText:
t = clean_text(shp.TextFrame.TextRange.Text)
if t:
return t[:90]
except Exception:
continue
except Exception:
pass
return ""
def data_uri(path):
with open(path, "rb") as f:
return "data:image/png;base64," + base64.b64encode(f.read()).decode("ascii")
def build_html(basename, slides):
"""slides: list of dicts {num, title, uri or None (blank)}"""
n = len(slides)
brand, date_label = parse_name(basename)
toc_rows, slide_rows = [], []
for s in slides:
num = s["num"]
title = html.escape(s["title"] or ("Slide %d" % num))
toc_rows.append(
'