89 lines
4.4 KiB
Python
89 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
||
"""Compile task-local Typst source to a checked PDF via a fixed command."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import json
|
||
import shutil
|
||
import tempfile
|
||
from contextlib import ExitStack
|
||
from pathlib import Path
|
||
|
||
from _docx_common import (SkillArgumentParser, WORD_OUTPUT_ROOT, find_program, input_file, load_json_argument,
|
||
output_file, publish_file, run_cli, run_program)
|
||
|
||
|
||
def resume_source(spec, directory):
|
||
if set(spec) - {"name", "contact", "summary", "sections"}:
|
||
raise ValueError("简历仅支持 name、contact、summary、sections")
|
||
if not isinstance(spec.get("name"), str) or not spec["name"].strip():
|
||
raise ValueError("name 必须是非空文本")
|
||
spec.setdefault("contact", [])
|
||
spec.setdefault("summary", "")
|
||
if not isinstance(spec["contact"], list) or not all(isinstance(x, str) for x in spec["contact"]):
|
||
raise ValueError("contact 必须是文本数组")
|
||
if not isinstance(spec["summary"], str) or not isinstance(spec.get("sections"), list):
|
||
raise ValueError("summary 必须是文本,sections 必须是数组")
|
||
for section in spec["sections"]:
|
||
if (not isinstance(section, dict) or set(section) != {"title", "items"}
|
||
or not isinstance(section["title"], str) or not isinstance(section["items"], list)
|
||
or not all(isinstance(item, str) for item in section["items"])):
|
||
raise ValueError("sections[] 必须包含文本 title 和文本数组 items")
|
||
(directory / "resume.json").write_text(json.dumps(spec, ensure_ascii=False), encoding="utf-8")
|
||
source = directory / "resume.typ"
|
||
shutil.copy2(Path(__file__).resolve().parent.parent / "assets/resume.typ", source)
|
||
return source
|
||
|
||
|
||
def main():
|
||
from pypdf import PdfReader
|
||
|
||
parser = SkillArgumentParser(description="把任务目录中的 Typst 文档编译为 PDF")
|
||
group = parser.add_mutually_exclusive_group(required=True)
|
||
group.add_argument("--input")
|
||
group.add_argument("--template", choices=["resume"])
|
||
parser.add_argument("--spec")
|
||
parser.add_argument("--spec-file")
|
||
parser.add_argument("--output", required=True)
|
||
parser.add_argument("--timeout", type=int, default=120)
|
||
parser.add_argument("--overwrite", action="store_true")
|
||
args = parser.parse_args()
|
||
with ExitStack() as stack:
|
||
if args.template:
|
||
task_root = WORD_OUTPUT_ROOT / "tmp"
|
||
task_root.mkdir(parents=True, exist_ok=True)
|
||
task_dir = Path(stack.enter_context(tempfile.TemporaryDirectory(prefix="typst-resume-", dir=task_root)))
|
||
source = resume_source(load_json_argument(args.spec, args.spec_file, label="简历说明"), task_dir)
|
||
else:
|
||
if args.spec is not None or args.spec_file is not None:
|
||
raise ValueError("spec/spec-file 仅用于 --template resume")
|
||
source = input_file(args.input, {".typ"})
|
||
try:
|
||
source.relative_to((WORD_OUTPUT_ROOT / "tmp").resolve())
|
||
except ValueError as exc:
|
||
raise ValueError("Typst 源文件及其素材必须放在 /usr/local/src/word/tmp/<任务名>/") from exc
|
||
if source.stat().st_size > 2 * 1024 * 1024:
|
||
raise ValueError("Typst 源文件不能超过 2 MiB")
|
||
destination = output_file(args.output, {".pdf"}, overwrite=args.overwrite)
|
||
descriptor, name = tempfile.mkstemp(prefix=".typst-", suffix=".pdf", dir=destination.parent)
|
||
os.close(descriptor)
|
||
temporary = Path(name)
|
||
try:
|
||
result = run_program([find_program("typst"), "compile", "--root", str(source.parent),
|
||
"--diagnostic-format", "short", str(source), str(temporary)],
|
||
timeout=args.timeout, cwd=source.parent)
|
||
reader = PdfReader(str(temporary))
|
||
page_count = len(reader.pages)
|
||
if reader.is_encrypted or page_count < 1:
|
||
raise ValueError("Typst 未生成可读取的 PDF 页面")
|
||
publish_file(temporary, destination, overwrite=args.overwrite)
|
||
finally:
|
||
temporary.unlink(missing_ok=True)
|
||
return {"path": str(destination), "source": args.input, "template": args.template, "page_count": page_count,
|
||
"warnings": result.stderr[-8000:], "requires_visual_review": True}
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(run_cli(main))
|