"""Headless soffice CLI helpers (convert/import) for UNO post-process.""" from __future__ import annotations import os import subprocess import sys from pathlib import Path from .uno_helpers import file_url_from_path from .uno_profile import ensure_profile_seeded, profile_dir def soffice_cli_path(soffice_path: str) -> str: """On Windows prefer ``soffice.com`` — ``soffice.exe`` opens the Start Center GUI.""" path = Path(soffice_path) if sys.platform == "win32": com = path.with_name("soffice.com") if com.is_file(): return str(com) return str(path) def _base_args(soffice_path: str, profile_root: Path) -> list[str]: ensure_profile_seeded(profile_root) return [ soffice_cli_path(soffice_path), f"-env:UserInstallation={file_url_from_path(str(profile_root))}", "--headless", "--nologo", "--norestore", "--nodefault", "--nofirststartwizard", "--nolockcheck", ] def convert_file( soffice_path: str, *, engine: str, src: str, out_dir: str, convert_to: str, timeout_s: float = 300.0, ) -> None: """Run ``soffice --convert-to …`` and wait until it finishes.""" profile_root = profile_dir(engine) cmd = [ *_base_args(soffice_path, profile_root), "--convert-to", convert_to, "--outdir", os.path.abspath(out_dir), os.path.abspath(src), ] completed = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout_s, cwd=str(Path(soffice_path).parent), creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if sys.platform == "win32" else 0, ) if completed.returncode != 0: tail = (completed.stderr or completed.stdout or "").strip() if len(tail) > 500: tail = tail[-500:] raise RuntimeError( f"convert-to {convert_to} failed (code {completed.returncode})" + (f": {tail}" if tail else "") ) stem = Path(src).stem expected = Path(out_dir) / f"{stem}.docx" if convert_to.startswith("docx") and not expected.is_file(): found = sorted(Path(out_dir).glob("*.docx")) if len(found) == 1: return raise RuntimeError( f"convert-to {convert_to}: output missing ({expected.name})" + (f", got {[p.name for p in found]}" if found else "") ) def import_docx_to_odt(soffice_path: str, *, engine: str, docx_path: str, out_dir: str) -> Path: """OpenOffice imports DOCX more reliably via native ODT round-trip.""" for spec in ("odt", 'odt:"writer8"'): try: convert_file( soffice_path, engine=engine, src=docx_path, out_dir=out_dir, convert_to=spec, ) odt = Path(out_dir) / f"{Path(docx_path).stem}.odt" if odt.is_file(): return odt except RuntimeError: continue raise RuntimeError(f"OpenOffice: не удалось импортировать DOCX → ODT: {docx_path}") def export_odt_to_docx(soffice_path: str, *, engine: str, odt_path: str, out_dir: str) -> Path: # OpenOffice on Windows treats spaces in ``docx:MS Word 2007 XML`` as extra # file arguments (Word / 2007 / XML). Use plain ``docx`` there. if engine == "openoffice": specs = ("docx",) else: specs = ("docx", "MS Word 2007 XML") for spec in specs: try: convert_file( soffice_path, engine=engine, src=odt_path, out_dir=out_dir, convert_to=spec, ) docx = Path(out_dir) / f"{Path(odt_path).stem}.docx" if docx.is_file(): return docx except RuntimeError: continue raise RuntimeError(f"OpenOffice: не удалось экспортировать ODT → DOCX: {odt_path}")