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

214 lines
7.0 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
from __future__ import annotations
import re
import sys
from contextlib import ExitStack
from pathlib import Path
from typing import Any
from _pdf_common import (
SkillArgumentParser,
input_pdf,
new_temp_pdf,
output_directory,
output_pdf,
parse_page_spec,
publish_temp_file,
run_cli,
)
def _parse_args(argv: list[str]):
parser = SkillArgumentParser(description="合并、拆分或旋转 PDF 页面")
operations = parser.add_subparsers(dest="operation", required=True)
merge = operations.add_parser("merge", help="合并多个 PDF")
merge.add_argument("--input", action="append", required=True, help="输入 PDF可重复")
merge.add_argument("--output", required=True, help="合并后的 PDF")
merge.add_argument("--overwrite", action="store_true")
split = operations.add_parser("split", help="按页码范围拆分 PDF")
split.add_argument("--input", required=True)
split.add_argument("--output-dir", required=True)
split.add_argument(
"--range",
dest="ranges",
action="append",
help="页码或页码范围,如 1-3可重复。缺省时每页一个文件",
)
split.add_argument("--overwrite", action="store_true")
rotate = operations.add_parser("rotate", help="旋转指定页面")
rotate.add_argument("--input", required=True)
rotate.add_argument("--output", required=True)
rotate.add_argument("--pages", help="页码列表,如 1,3-5缺省时旋转全部页面")
rotate.add_argument("--degrees", type=int, choices=(90, 180, 270), required=True)
rotate.add_argument("--overwrite", action="store_true")
return parser.parse_args(argv)
def _metadata(reader) -> dict[str, str]:
return {
str(key): str(value)
for key, value in (reader.metadata or {}).items()
if value is not None
}
def _merge(args) -> dict[str, Any]:
from pypdf import PdfReader, PdfWriter
inputs = [input_pdf(value) for value in args.input]
if len(inputs) < 2:
raise ValueError("merge 至少需要两个 --input")
output = output_pdf(args.output, args.overwrite)
if output in inputs:
raise ValueError("输出文件不能与输入文件相同")
temporary = new_temp_pdf(output)
writer = PdfWriter()
try:
with ExitStack() as stack:
readers = []
for path in inputs:
stream = stack.enter_context(path.open("rb"))
reader = PdfReader(stream, strict=False)
if reader.is_encrypted:
raise ValueError(f"PDF 已加密,无法合并:{path}")
readers.append(reader)
for page in reader.pages:
writer.add_page(page)
if readers:
writer.add_metadata(_metadata(readers[0]))
with temporary.open("wb") as stream:
writer.write(stream)
page_count = len(writer.pages)
publish_temp_file(temporary, output, args.overwrite)
finally:
writer.close()
temporary.unlink(missing_ok=True)
return {
"operation": "merge",
"inputs": [str(path) for path in inputs],
"output": str(output),
"page_count": page_count,
}
def _split(args) -> dict[str, Any]:
from pypdf import PdfReader, PdfWriter
path = input_pdf(args.input)
destination = output_directory(args.output_dir)
with path.open("rb") as stream:
reader = PdfReader(stream, strict=False)
if reader.is_encrypted:
raise ValueError("PDF 已加密,无法拆分")
page_count = len(reader.pages)
if args.ranges:
groups = [
(value, parse_page_spec(value, page_count))
for value in args.ranges
]
else:
groups = [(str(page), [page]) for page in range(1, page_count + 1)]
outputs = []
for label, pages in groups:
safe_label = re.sub(r"[^0-9_-]+", "_", label.replace(",", "_"))
output = destination / f"{path.stem}_pages_{safe_label}.pdf"
if output.exists() and not args.overwrite:
raise FileExistsError(f"目标文件已存在:{output}")
outputs.append((output, pages))
created = []
for output, pages in outputs:
temporary = new_temp_pdf(output)
writer = PdfWriter()
try:
for page_number in pages:
writer.add_page(reader.pages[page_number - 1])
writer.add_metadata(_metadata(reader))
with temporary.open("wb") as output_stream:
writer.write(output_stream)
publish_temp_file(temporary, output, args.overwrite)
created.append(
{
"path": str(output),
"pages": pages,
"page_count": len(pages),
}
)
finally:
writer.close()
temporary.unlink(missing_ok=True)
return {
"operation": "split",
"input": str(path),
"output_dir": str(destination),
"source_page_count": page_count,
"files": created,
}
def _rotate(args) -> dict[str, Any]:
from pypdf import PdfReader, PdfWriter
path = input_pdf(args.input)
output = output_pdf(args.output, args.overwrite)
if output == path:
raise ValueError("输出文件不能与输入文件相同")
temporary = new_temp_pdf(output)
writer = PdfWriter()
try:
with path.open("rb") as stream:
reader = PdfReader(stream, strict=False)
if reader.is_encrypted:
raise ValueError("PDF 已加密,无法旋转")
page_count = len(reader.pages)
selected = set(parse_page_spec(args.pages, page_count))
for page_number, page in enumerate(reader.pages, start=1):
if page_number in selected:
page.rotate(args.degrees)
writer.add_page(page)
writer.add_metadata(_metadata(reader))
with temporary.open("wb") as output_stream:
writer.write(output_stream)
publish_temp_file(temporary, output, args.overwrite)
finally:
writer.close()
temporary.unlink(missing_ok=True)
return {
"operation": "rotate",
"input": str(path),
"output": str(output),
"page_count": page_count,
"rotated_pages": sorted(selected),
"degrees": args.degrees,
}
def _execute(args) -> dict[str, Any]:
if args.operation == "merge":
return _merge(args)
if args.operation == "split":
return _split(args)
if args.operation == "rotate":
return _rotate(args)
raise ValueError(f"不支持的操作:{args.operation}")
def main(argv: list[str] | None = None) -> int:
arguments = sys.argv[1:] if argv is None else argv
return run_cli(lambda: _execute(_parse_args(arguments)))
if __name__ == "__main__":
raise SystemExit(main())