"""舊版 OLE 二進位格式(.doc/.xls/.ppt)→ 現代格式(.docx/.xlsx/.pptx)升版。 以 LibreOffice headless 在本機升版(deterministic、內容不外流,AGENTS.md §1.1), 升版後由 convert.py 回頭走既有 from_docx / from_xlsx / from_pptx 轉換器。 升版產物是暫時中介檔,不進 raw/;raw/originals 仍保存真正的舊格式原始檔(§1.4)。 可獨立執行:python tools/convert/from_legacy.py [--out DIR] """ import argparse import os import pathlib import shutil import subprocess import sys import tempfile LEGACY_TARGET = {".doc": "docx", ".xls": "xlsx", ".ppt": "pptx"} # 常見 Windows 安裝路徑;其他平台靠 PATH(soffice/libreoffice)或 SOFFICE_BIN 覆寫。 _WIN_CANDIDATES = ( r"C:\Program Files\LibreOffice\program\soffice.exe", r"C:\Program Files (x86)\LibreOffice\program\soffice.exe", ) def find_soffice(): """定位 LibreOffice 執行檔。SOFFICE_BIN 環境變數優先(唯一允許的覆寫來源)。""" env = os.environ.get("SOFFICE_BIN") if env and pathlib.Path(env).is_file(): return env for c in _WIN_CANDIDATES: if pathlib.Path(c).is_file(): return c return shutil.which("soffice") or shutil.which("libreoffice") def upgrade(src, out_dir, timeout=180): """升版 src 至 out_dir,回傳升版後檔案路徑。 找不到 LibreOffice、格式不支援、或升版未產出檔案時拋 RuntimeError(由 convert.py 逐檔捕捉並回報,不中斷批次)。 """ src = pathlib.Path(src) ext = src.suffix.lower() if ext not in LEGACY_TARGET: raise RuntimeError(f"非舊版格式:{src.name}") soffice = find_soffice() if not soffice: raise RuntimeError( "找不到 LibreOffice(soffice);.doc/.xls/.ppt 升版需要它。" "請安裝 LibreOffice,或以 SOFFICE_BIN 環境變數指定執行檔路徑。") out_dir = pathlib.Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) target = LEGACY_TARGET[ext] # 每次呼叫用獨立 user profile,避免 headless 並行/殘留 profile 造成的鎖定失敗 # (LibreOffice headless 的已知痛點,§13.3:真實環境不是規格書上的理想值)。 profile = out_dir / ".lo-profile" try: subprocess.run( [soffice, f"-env:UserInstallation=file:///{profile.as_posix().lstrip('/')}", "--headless", "--convert-to", target, "--outdir", str(out_dir), str(src)], check=True, capture_output=True, text=True, timeout=timeout) except subprocess.CalledProcessError as e: raise RuntimeError(f"LibreOffice 升版失敗(exit {e.returncode}):{e.stderr.strip()}") except subprocess.TimeoutExpired: raise RuntimeError(f"LibreOffice 升版逾時(>{timeout}s):{src.name}") finally: shutil.rmtree(profile, ignore_errors=True) out = out_dir / f"{src.stem}.{target}" if not out.is_file(): raise RuntimeError(f"LibreOffice 未產出預期檔案:{out.name}") return out def main(argv=None): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("src") ap.add_argument("--out", help="升版產物輸出目錄(預設暫存目錄)") a = ap.parse_args(argv) out_dir = pathlib.Path(a.out) if a.out else pathlib.Path(tempfile.mkdtemp(prefix="ppqa-legacy-")) try: print(upgrade(a.src, out_dir)) except RuntimeError as e: print(f"error: {e}", file=sys.stderr) raise SystemExit(1) if __name__ == "__main__": main()