Files
Igor20264 daa84f8bce
Example document / build (push) Has been cancelled
Python application / build (push) Has been cancelled
v0.5.6
2026-09-22 18:49:09 +03:00

126 lines
4.0 KiB
Python

"""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}")