126 lines
4.0 KiB
Python
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}")
|