Files

138 lines
4.1 KiB
Python

from __future__ import annotations
import argparse
import uuid
from collections.abc import Sequence
from pathlib import Path
from pypdf import PdfReader, PdfWriter
from copienator import (
EvaluationWorkspace,
ExitCode,
execute,
standard_parser,
workspace_from_args,
)
def copy_pdfs(directory: Path) -> list[Path]:
directory = directory.expanduser().resolve()
if not directory.is_dir():
raise NotADirectoryError(f"Dossier introuvable : {directory}")
return sorted(
(
path
for path in directory.glob("*.pdf")
if path.name.casefold() not in {"enonce.pdf", "énoncé.pdf"}
),
key=lambda path: path.name.casefold(),
)
def rotate_pdf(path: Path) -> None:
temporary = path.with_name(f".{path.stem}.rotate-{uuid.uuid4().hex}.pdf")
try:
with path.open("rb") as source, temporary.open("wb") as destination:
reader = PdfReader(source)
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page.rotate(180))
if reader.metadata:
metadata = {
str(key): str(value)
for key, value in reader.metadata.items()
if value is not None
}
writer.add_metadata(metadata)
writer.write(destination)
temporary.replace(path)
finally:
if temporary.exists():
temporary.unlink()
def rotate_all(directory: Path) -> int:
files = copy_pdfs(directory)
for path in files:
rotate_pdf(path)
print(f"Rotated: {path.name}")
if not files:
print("No PDF copies found.")
return len(files)
def rename_all(directory: Path) -> list[tuple[Path, Path]]:
directory = directory.expanduser().resolve()
files = copy_pdfs(directory)
width = max(2, len(str(len(files))))
plan = [
(source, directory / f"Copie{index:0{width}d}.pdf")
for index, source in enumerate(files, start=1)
]
staged: list[tuple[Path, Path, Path]] = []
try:
for source, destination in plan:
temporary = directory / f".copienator-rename-{uuid.uuid4().hex}.pdf"
source.replace(temporary)
staged.append((source, temporary, destination))
completed: list[tuple[Path, Path, Path]] = []
try:
for source, temporary, destination in staged:
temporary.replace(destination)
completed.append((source, temporary, destination))
print(f"Renamed: {source.name} -> {destination.name}")
except OSError:
for _source, temporary, destination in completed:
if destination.exists():
destination.replace(temporary)
raise
except OSError:
for source, temporary, _destination in staged:
if temporary.exists():
temporary.replace(source)
raise
if not plan:
print("No PDF copies found.")
return [(source, destination) for source, _temporary, destination in staged]
def build_parser() -> argparse.ArgumentParser:
parser = standard_parser("Prepare scanned PDF copies.")
subparsers = parser.add_subparsers(dest="operation", required=True)
for operation in ("rotate", "rename"):
subparser = subparsers.add_parser(operation)
subparser.add_argument("evaluation", type=Path, help="Evaluation directory")
subparser.add_argument(
"--verbose",
action="store_true",
default=argparse.SUPPRESS,
help="Show a traceback when an unexpected error occurs",
)
return parser
def run(workspace: EvaluationWorkspace, *, operation: str) -> ExitCode:
if operation == "rotate":
rotate_all(workspace.root)
else:
rename_all(workspace.root)
return ExitCode.SUCCESS
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
return execute(
parser,
argv,
lambda args: run(workspace_from_args(args), operation=args.operation),
)
if __name__ == "__main__":
raise SystemExit(main())