pdf-merge/pdf_merge.py
2026-04-11 12:19:45 +03:00

156 lines
5.0 KiB
Python
Executable File
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
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pypdf>=4.0.0",
# "pyyaml>=6.0",
# ]
# ///
"""
pdf_merge.py — собирает PDF из кусков других PDF по YAML-конфигу.
Использование:
uv run pdf_merge.py [config.yaml]
Если конфиг не указан явно — ищет merge.yaml в текущей директории.
"""
import sys
import re
from pathlib import Path
import yaml
from pypdf import PdfReader, PdfWriter
# ---------------------------------------------------------------------------
# Парсинг спецификации страниц
# ---------------------------------------------------------------------------
def parse_pages(spec: str | int | list, total: int) -> list[int]:
"""
Принимает спецификацию страниц и возвращает список 0-based индексов.
Форматы (1-based нумерация в конфиге):
"1-5" -> [0, 1, 2, 3, 4]
"1,3,5" -> [0, 2, 4]
"1-3,7,9-11" -> [0, 1, 2, 6, 8, 9, 10]
"all" -> все страницы
3 -> [2] (одно число как int)
[1, 3, 5] -> [0, 2, 4] (список чисел)
"""
if spec is None or str(spec).strip().lower() == "all":
return list(range(total))
if isinstance(spec, int):
spec = str(spec)
if isinstance(spec, list):
spec = ",".join(str(s) for s in spec)
pages = []
for part in re.split(r"[,;]\s*", str(spec).strip()):
part = part.strip()
if not part:
continue
m = re.fullmatch(r"(\d+)\s*-\s*(\d+)", part)
if m:
start, end = int(m.group(1)), int(m.group(2))
if start < 1 or end > total or start > end:
raise ValueError(
f"Диапазон {part} выходит за границы документа (всего {total} стр.)"
)
pages.extend(range(start - 1, end))
elif re.fullmatch(r"\d+", part):
n = int(part)
if n < 1 or n > total:
raise ValueError(
f"Страница {n} не существует (всего {total} стр.)"
)
pages.append(n - 1)
else:
raise ValueError(f"Не удалось разобрать спецификацию страниц: '{part}'")
return pages
# ---------------------------------------------------------------------------
# Основная логика
# ---------------------------------------------------------------------------
def merge(config_path: Path) -> None:
with open(config_path, encoding="utf-8") as f:
cfg = yaml.safe_load(f)
output_path = Path(cfg.get("output", "output.pdf"))
base_dir = config_path.parent # файлы ищем относительно конфига
steps: list[dict] = cfg.get("pages", [])
if not steps:
print("Ошибка: в конфиге нет секции 'pages'.", file=sys.stderr)
sys.exit(1)
writer = PdfWriter()
total_added = 0
for i, step in enumerate(steps, 1):
if "file" not in step:
print(f"Шаг {i}: нет поля 'file', пропускаю.", file=sys.stderr)
continue
pdf_path = base_dir / step["file"]
if not pdf_path.exists():
print(f"Шаг {i}: файл не найден: {pdf_path}", file=sys.stderr)
sys.exit(1)
reader = PdfReader(str(pdf_path))
total_pages = len(reader.pages)
spec = step.get("pages", "all")
try:
indices = parse_pages(spec, total_pages)
except ValueError as e:
print(f"Шаг {i} ({step['file']}): {e}", file=sys.stderr)
sys.exit(1)
for idx in indices:
writer.add_page(reader.pages[idx])
label = step.get("label", "")
label_str = f" [{label}]" if label else ""
print(
f" + {step['file']}{label_str}: "
f"страниц {len(indices)} (из {total_pages}) — "
f"индексы {[x+1 for x in indices]}"
)
total_added += len(indices)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as out:
writer.write(out)
print(f"\nГотово: {output_path} ({total_added} стр.)")
# ---------------------------------------------------------------------------
# Entrypoint
# ---------------------------------------------------------------------------
def main() -> None:
if len(sys.argv) > 1:
config_path = Path(sys.argv[1])
else:
config_path = Path("merge.yaml")
if not config_path.exists():
print(f"Конфиг не найден: {config_path}", file=sys.stderr)
print("Использование: uv run pdf_merge.py [config.yaml]", file=sys.stderr)
sys.exit(1)
print(f"Конфиг: {config_path}")
merge(config_path)
if __name__ == "__main__":
main()