151 lines
4.6 KiB
Python
151 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any, Callable, NoReturn, Optional
|
|
|
|
|
|
class SkillArgumentParser(argparse.ArgumentParser):
|
|
def error(self, message: str) -> NoReturn:
|
|
raise ValueError(f"参数错误:{message}")
|
|
|
|
|
|
def emit(payload: dict[str, Any]) -> None:
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
|
|
|
|
def failure_message(exc: Exception) -> str:
|
|
if isinstance(exc, FileExistsError):
|
|
return str(exc)
|
|
if isinstance(exc, PermissionError):
|
|
return "文件处理失败:没有目标路径的访问权限"
|
|
if isinstance(exc, OSError):
|
|
return f"文件处理失败:{exc}"
|
|
return str(exc)
|
|
|
|
|
|
def run_cli(action: Callable[[], dict[str, Any]]) -> int:
|
|
try:
|
|
result = action()
|
|
except Exception as exc:
|
|
emit({"ok": False, "error": failure_message(exc)})
|
|
return 1
|
|
emit({"ok": True, **result})
|
|
return 0
|
|
|
|
|
|
def input_pdf(value: str) -> Path:
|
|
path = Path(value).expanduser().resolve()
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"PDF 文件不存在:{path}")
|
|
if not path.is_file():
|
|
raise ValueError(f"PDF 路径不是文件:{path}")
|
|
if path.suffix.lower() != ".pdf":
|
|
raise ValueError("PDF 输入路径必须以 .pdf 结尾")
|
|
if path.stat().st_size <= 0:
|
|
raise ValueError("PDF 文件为空")
|
|
return path
|
|
|
|
|
|
def output_pdf(value: str, overwrite: bool) -> Path:
|
|
path = Path(value).expanduser().resolve()
|
|
if path.suffix.lower() != ".pdf":
|
|
raise ValueError("PDF 输出路径必须以 .pdf 结尾")
|
|
if path.exists() and not overwrite:
|
|
raise FileExistsError(f"目标文件已存在:{path}")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def output_directory(value: str) -> Path:
|
|
path = Path(value).expanduser().resolve()
|
|
if path.exists() and not path.is_dir():
|
|
raise ValueError(f"输出路径不是目录:{path}")
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
|
|
def new_temp_pdf(output: Path) -> Path:
|
|
descriptor, name = tempfile.mkstemp(
|
|
prefix=f".{output.name}.",
|
|
suffix=".part.pdf",
|
|
dir=str(output.parent),
|
|
)
|
|
os.close(descriptor)
|
|
return Path(name)
|
|
|
|
|
|
def publish_temp_file(temp_path: Path, output: Path, overwrite: bool) -> None:
|
|
if overwrite:
|
|
os.replace(temp_path, output)
|
|
return
|
|
try:
|
|
os.link(temp_path, output)
|
|
except FileExistsError as exc:
|
|
raise FileExistsError(f"目标文件已存在:{output}") from exc
|
|
temp_path.unlink()
|
|
|
|
|
|
def parse_page_spec(value: Optional[str], page_count: int) -> list[int]:
|
|
if page_count < 1:
|
|
return []
|
|
if value is None or not value.strip():
|
|
return list(range(1, page_count + 1))
|
|
|
|
pages: set[int] = set()
|
|
for raw_part in value.split(","):
|
|
part = raw_part.strip()
|
|
if not part:
|
|
continue
|
|
if "-" in part:
|
|
pieces = part.split("-", 1)
|
|
try:
|
|
start = int(pieces[0])
|
|
end = int(pieces[1])
|
|
except ValueError as exc:
|
|
raise ValueError(f"页码范围格式错误:{part}") from exc
|
|
if start > end:
|
|
raise ValueError(f"页码范围起始值不能大于结束值:{part}")
|
|
else:
|
|
try:
|
|
start = end = int(part)
|
|
except ValueError as exc:
|
|
raise ValueError(f"页码格式错误:{part}") from exc
|
|
|
|
if start < 1 or end > page_count:
|
|
raise ValueError(f"页码必须在 1 到 {page_count} 之间:{part}")
|
|
pages.update(range(start, end + 1))
|
|
|
|
if not pages:
|
|
raise ValueError("页码范围不能为空")
|
|
return sorted(pages)
|
|
|
|
|
|
def selected_page_window(
|
|
page_count: int,
|
|
start_page: int,
|
|
end_page: Optional[int],
|
|
max_pages: int,
|
|
) -> tuple[int, int, Optional[int]]:
|
|
if page_count < 1:
|
|
raise ValueError("PDF 没有可处理的页面")
|
|
if start_page < 1 or start_page > page_count:
|
|
raise ValueError(f"start-page 必须在 1 到 {page_count} 之间")
|
|
if max_pages < 1:
|
|
raise ValueError("max-pages 必须大于 0")
|
|
|
|
requested_end = page_count if end_page is None else end_page
|
|
if requested_end < start_page or requested_end > page_count:
|
|
raise ValueError(
|
|
f"end-page 必须在 start-page 到 {page_count} 之间"
|
|
)
|
|
|
|
actual_end = min(requested_end, start_page + max_pages - 1)
|
|
next_page = actual_end + 1 if actual_end < requested_end else None
|
|
return start_page, actual_end, next_page
|