137 lines
4.1 KiB
Python
137 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())
|