accept_changes.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. """Accept all tracked changes in a DOCX file using LibreOffice.
  2. Requires LibreOffice (soffice) to be installed.
  3. """
  4. import argparse
  5. import logging
  6. import shutil
  7. import subprocess
  8. from pathlib import Path
  9. from office.soffice import get_soffice_env
  10. logger = logging.getLogger(__name__)
  11. LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
  12. MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
  13. ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
  14. <!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
  15. <script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
  16. Sub AcceptAllTrackedChanges()
  17. Dim document As Object
  18. Dim dispatcher As Object
  19. document = ThisComponent.CurrentController.Frame
  20. dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
  21. dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
  22. ThisComponent.store()
  23. ThisComponent.close(True)
  24. End Sub
  25. </script:module>"""
  26. def accept_changes(
  27. input_file: str,
  28. output_file: str,
  29. ) -> tuple[None, str]:
  30. input_path = Path(input_file)
  31. output_path = Path(output_file)
  32. if not input_path.exists():
  33. return None, f"Error: Input file not found: {input_file}"
  34. if not input_path.suffix.lower() == ".docx":
  35. return None, f"Error: Input file is not a DOCX file: {input_file}"
  36. try:
  37. output_path.parent.mkdir(parents=True, exist_ok=True)
  38. shutil.copy2(input_path, output_path)
  39. except Exception as e:
  40. return None, f"Error: Failed to copy input file to output location: {e}"
  41. if not _setup_libreoffice_macro():
  42. return None, "Error: Failed to setup LibreOffice macro"
  43. cmd = [
  44. "soffice",
  45. "--headless",
  46. f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
  47. "--norestore",
  48. "vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
  49. str(output_path.absolute()),
  50. ]
  51. try:
  52. result = subprocess.run(
  53. cmd,
  54. capture_output=True,
  55. text=True,
  56. timeout=30,
  57. check=False,
  58. env=get_soffice_env(),
  59. )
  60. except subprocess.TimeoutExpired:
  61. return (
  62. None,
  63. f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
  64. )
  65. if result.returncode != 0:
  66. return None, f"Error: LibreOffice failed: {result.stderr}"
  67. return (
  68. None,
  69. f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
  70. )
  71. def _setup_libreoffice_macro() -> bool:
  72. macro_dir = Path(MACRO_DIR)
  73. macro_file = macro_dir / "Module1.xba"
  74. if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
  75. return True
  76. if not macro_dir.exists():
  77. subprocess.run(
  78. [
  79. "soffice",
  80. "--headless",
  81. f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
  82. "--terminate_after_init",
  83. ],
  84. capture_output=True,
  85. timeout=10,
  86. check=False,
  87. env=get_soffice_env(),
  88. )
  89. macro_dir.mkdir(parents=True, exist_ok=True)
  90. try:
  91. macro_file.write_text(ACCEPT_CHANGES_MACRO)
  92. return True
  93. except Exception as e:
  94. logger.warning(f"Failed to setup LibreOffice macro: {e}")
  95. return False
  96. if __name__ == "__main__":
  97. parser = argparse.ArgumentParser(
  98. description="Accept all tracked changes in a DOCX file"
  99. )
  100. parser.add_argument("input_file", help="Input DOCX file with tracked changes")
  101. parser.add_argument(
  102. "output_file", help="Output DOCX file (clean, no tracked changes)"
  103. )
  104. args = parser.parse_args()
  105. _, message = accept_changes(args.input_file, args.output_file)
  106. print(message)
  107. if "Error" in message:
  108. raise SystemExit(1)