118 lines
3.6 KiB
Python
118 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
||
"""Office to PDF using the preinstalled LibreOffice, isolated per invocation."""
|
||
|
||
from pathlib import Path
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
|
||
from _pdf_common import (
|
||
SkillArgumentParser,
|
||
output_pdf,
|
||
new_temp_pdf,
|
||
publish_temp_file,
|
||
run_cli,
|
||
)
|
||
|
||
FORMATS = {
|
||
".docx",
|
||
".doc",
|
||
".odt",
|
||
".rtf",
|
||
".pptx",
|
||
".ppt",
|
||
".odp",
|
||
".xlsx",
|
||
".xls",
|
||
".ods",
|
||
}
|
||
|
||
|
||
def convert(args):
|
||
from pypdf import PdfReader
|
||
|
||
source = Path(args.input).expanduser().resolve()
|
||
if (
|
||
not source.is_file()
|
||
or source.suffix.lower() not in FORMATS
|
||
or not 0 < source.stat().st_size <= 25 * 1024 * 1024
|
||
):
|
||
raise ValueError(
|
||
"输入需为不超过 25 MiB 的本地 Office 文档;不支持把 PDF 直接反向转成可编辑 Office"
|
||
)
|
||
if not 1 <= args.timeout <= 600:
|
||
raise ValueError("timeout 必须在 1–600 秒之间")
|
||
executable = shutil.which("soffice") or shutil.which("libreoffice")
|
||
if not executable:
|
||
raise RuntimeError("基础镜像缺少 LibreOffice,需要更新镜像")
|
||
target = output_pdf(args.output, args.overwrite)
|
||
temporary = new_temp_pdf(target)
|
||
try:
|
||
with tempfile.TemporaryDirectory(prefix="pdf-office-") as folder:
|
||
root = Path(folder)
|
||
incoming = root / "input"
|
||
outgoing = root / "output"
|
||
profile = root / "profile"
|
||
incoming.mkdir()
|
||
outgoing.mkdir()
|
||
profile.mkdir()
|
||
local = incoming / ("source" + source.suffix.lower())
|
||
shutil.copyfile(source, local)
|
||
command = [
|
||
executable,
|
||
"-env:UserInstallation=" + profile.as_uri(),
|
||
"--headless",
|
||
"--nologo",
|
||
"--nodefault",
|
||
"--nofirststartwizard",
|
||
"--convert-to",
|
||
"pdf",
|
||
"--outdir",
|
||
str(outgoing),
|
||
str(local),
|
||
]
|
||
completed = subprocess.run(
|
||
command,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=args.timeout,
|
||
check=False,
|
||
)
|
||
result = outgoing / "source.pdf"
|
||
if completed.returncode or not result.is_file():
|
||
raise RuntimeError(
|
||
"Office 转 PDF 失败:"
|
||
+ (completed.stderr or completed.stdout)[-1500:]
|
||
)
|
||
count = len(PdfReader(result).pages)
|
||
if not count:
|
||
raise ValueError("转换结果没有页面")
|
||
shutil.copyfile(result, temporary)
|
||
publish_temp_file(temporary, target, args.overwrite)
|
||
finally:
|
||
temporary.unlink(missing_ok=True)
|
||
return {
|
||
"source": str(source),
|
||
"path": str(target),
|
||
"page_count": count,
|
||
"engine": "libreoffice",
|
||
"requires_visual_review": True,
|
||
"note": "转换前需在对应 Office skill 中重算公式并核对字体、图表和打印范围。",
|
||
}
|
||
|
||
|
||
def main(argv=None):
|
||
parser = SkillArgumentParser(description="Office 文档导出 PDF,源文件保持不变")
|
||
parser.add_argument("--input", required=True)
|
||
parser.add_argument("--output", required=True)
|
||
parser.add_argument("--timeout", type=int, default=180)
|
||
parser.add_argument("--overwrite", action="store_true")
|
||
return run_cli(
|
||
lambda: convert(parser.parse_args(sys.argv[1:] if argv is None else argv))
|
||
)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|