"""Accept tracked changes into a new DOCX using an isolated LibreOffice profile.""" import argparse import shutil import subprocess import tempfile import zipfile from pathlib import Path from defusedxml import ElementTree from office.soffice import run_soffice MACRO = ''' Sub AcceptAllTrackedChanges() Dim dispatcher As Object dispatcher = createUnoService("com.sun.star.frame.DispatchHelper") dispatcher.executeDispatch(ThisComponent.CurrentController.Frame, ".uno:AcceptAllTrackedChanges", "", 0, Array()) ThisComponent.store() ThisComponent.close(True) End Sub ''' WORD_NS = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" def accept_changes(input_file, output_file, timeout=120): source = Path(input_file).resolve() output = Path(output_file).resolve() if source == output or not source.is_file() or source.suffix.lower() != ".docx": return None, "Input must be an existing DOCX and output must be a different file" output.parent.mkdir(parents=True, exist_ok=True) try: with tempfile.TemporaryDirectory(prefix="docx-accept-", ignore_cleanup_errors=True) as temporary: root = Path(temporary).resolve() profile = root / "profile" profile_arg = f"-env:UserInstallation={profile.as_uri()}" run_soffice([profile_arg, "--headless", "--terminate_after_init"], check=True, capture_output=True, timeout=timeout) macro_dir = profile / "user/basic/Standard" macro_dir.mkdir(parents=True, exist_ok=True) (macro_dir / "Module1.xba").write_text(MACRO, encoding="utf-8") candidate = root / "accepted.docx" shutil.copy2(source, candidate) before = candidate.stat().st_mtime_ns run_soffice([ profile_arg, "--headless", "--norestore", "vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application", str(candidate), ], check=True, capture_output=True, timeout=timeout) if candidate.stat().st_mtime_ns == before: return None, "LibreOffice did not save the document" with zipfile.ZipFile(candidate) as archive: for name in archive.namelist(): if not name.startswith("word/") or not name.endswith(".xml"): continue document = ElementTree.fromstring(archive.read(name)) if any(element.tag in {WORD_NS + "ins", WORD_NS + "del", WORD_NS + "moveFrom", WORD_NS + "moveTo"} or (element.tag.startswith(WORD_NS) and element.tag.endswith("Change")) for element in document.iter()): return None, f"Tracked changes remain in {name}" shutil.copy2(candidate, output) return None, f"Accepted tracked changes: {output}" except (OSError, subprocess.SubprocessError, zipfile.BadZipFile) as exc: return None, f"Failed to accept tracked changes: {exc}" if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("input") parser.add_argument("output") parser.add_argument("--timeout", type=int, default=120) args = parser.parse_args() _, message = accept_changes(args.input, args.output, args.timeout) print(message) raise SystemExit(0 if message.startswith("Accepted tracked changes:") else 1)