wechat-robot-skills/skills/pdf/scripts/compile_latex.py

113 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Compile a LaTeX project with cached Tectonic resources and shell escape disabled."""
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
from _pdf_common import (
SkillArgumentParser,
output_pdf,
new_temp_pdf,
publish_temp_file,
run_cli,
)
def compile_document(args):
from pypdf import PdfReader
source = Path(args.input).expanduser().resolve()
if (
not source.is_file()
or source.suffix.lower() != ".tex"
or not 0 < source.stat().st_size <= 2 * 1024 * 1024
):
raise ValueError("输入需为不超过 2 MiB 的本地 .tex 文件")
if not 1 <= args.timeout <= 600:
raise ValueError("timeout 必须在 1–600 秒之间")
executable = shutil.which("tectonic")
if not executable:
raise RuntimeError("基础镜像缺少预置 Tectonic,需要更新镜像")
target = output_pdf(args.output, args.overwrite)
temporary = new_temp_pdf(target)
try:
with tempfile.TemporaryDirectory(prefix="pdf-latex-") as folder:
command = [
executable,
"--untrusted",
"--only-cached",
"--keep-logs",
"--outdir",
folder,
str(source),
]
completed = subprocess.run(
command,
cwd=source.parent,
capture_output=True,
text=True,
timeout=args.timeout,
check=False,
)
result = Path(folder) / (source.stem + ".pdf")
messages = (completed.stdout + "\n" + completed.stderr).splitlines()
if completed.returncode or not result.is_file():
detail = "\n".join(messages[-15:])
raise RuntimeError(
"LaTeX 编译失败;缺失 TeX 包需在基础镜像构建时预置,任务中不下载:"
+ detail
)
count = len(PdfReader(result).pages)
if not count:
raise ValueError("LaTeX 没有生成有效 PDF")
logfile = Path(folder) / (source.stem + ".log")
if logfile.exists():
messages += logfile.read_text(errors="replace").splitlines()
warnings = list(
dict.fromkeys(
line.strip()
for line in messages
if re.search(
r"warning:|Overfull|Missing character|undefined references",
line,
re.I,
)
)
)[:30]
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": "tectonic",
"dependency_mode": "cached-only",
"warnings": warnings,
"requires_visual_review": True,
}
def main(argv=None):
parser = SkillArgumentParser(
description="固定 LaTeX 编译接口,禁用 shell escape,只使用镜像内缓存包"
)
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: compile_document(
parser.parse_args(sys.argv[1:] if argv is None else argv)
)
)
if __name__ == "__main__":
raise SystemExit(main())