scripts/pdf_meta.py
scripts/pdf_meta.pyBrowse 21 files
991 tokens
4,458 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Document metadata and file attachments for PDFs (pypdf).3 4Modes (one required):5 --set-meta set metadata keys given via --title/--author/...6 --clear-meta drop all document info metadata7 --attach FILE embed a file attachment8 --list-attachments list embedded attachment names9 --extract-attachments DIR write all attachments into DIR10 11Metadata note: values are stored in the classic DocInfo dictionary12(Title/Author/Subject/Keywords). XMP metadata, if present, is not13rewritten and may disagree in sophisticated viewers.14"""15from __future__ import annotations16 17import argparse18import json19import os20import sys21from pathlib import Path22 23 24def main() -> int:25 for stream in (sys.stdout, sys.stderr):26 try:27 stream.reconfigure(encoding="utf-8")28 except Exception:29 pass30 parser = argparse.ArgumentParser(description="Set/clear PDF metadata; manage attachments.")31 parser.add_argument("pdf", help="Input PDF path")32 mode = parser.add_mutually_exclusive_group(required=True)33 mode.add_argument("--set-meta", action="store_true", help="Set metadata fields")34 mode.add_argument("--clear-meta", action="store_true", help="Remove all DocInfo metadata")35 mode.add_argument("--attach", metavar="FILE", help="Embed FILE as an attachment")36 mode.add_argument("--list-attachments", action="store_true", help="List attachment names")37 mode.add_argument("--extract-attachments", metavar="DIR", help="Extract attachments into DIR")38 parser.add_argument("-o", "--output", help="Output PDF (required for write modes)")39 parser.add_argument("--title")40 parser.add_argument("--author")41 parser.add_argument("--subject")42 parser.add_argument("--keywords")43 parser.add_argument("--password", help="Password if the input is encrypted")44 args = parser.parse_args()45 46 try:47 from pypdf import PdfReader, PdfWriter48 except ImportError:49 print("Missing dependency: install with 'python3 -m pip install pypdf'", file=sys.stderr)50 return 251 52 reader = PdfReader(args.pdf)53 if reader.is_encrypted:54 if args.password is None or not reader.decrypt(args.password):55 print("Error: input is encrypted; pass --password", file=sys.stderr)56 return 357 58 if args.list_attachments:59 names = list(reader.attachments.keys())60 json.dump({"attachment_count": len(names), "attachments": names}, sys.stdout,61 ensure_ascii=False, indent=2)62 print()63 return 064 65 if args.extract_attachments:66 out_dir = Path(args.extract_attachments)67 out_dir.mkdir(parents=True, exist_ok=True)68 written = []69 for name, contents in reader.attachments.items():70 data = contents[0] if isinstance(contents, list) else contents71 safe = os.path.basename(name) or "attachment.bin"72 target = out_dir / safe73 with open(target, "wb") as fh:74 fh.write(bytes(data))75 written.append(str(target))76 json.dump({"extracted": written}, sys.stdout, ensure_ascii=False, indent=2)77 print()78 return 079 80 if not args.output:81 print("Error: -o/--output is required for write modes", file=sys.stderr)82 return 483 84 writer = PdfWriter()85 writer.append(reader)86 87 if args.set_meta:88 meta = {}89 for key, value in ((f"/{k.capitalize()}", getattr(args, k))90 for k in ("title", "author", "subject", "keywords")):91 if value is not None:92 meta[key] = value93 if not meta:94 print("Error: --set-meta needs at least one of --title/--author/--subject/--keywords",95 file=sys.stderr)96 return 497 writer.add_metadata(meta)98 result = {"output": args.output, "set": {k.lstrip("/"): v for k, v in meta.items()}}99 elif args.clear_meta:100 writer.metadata = None101 result = {"output": args.output, "cleared": True}102 else: # --attach103 attach_path = Path(args.attach)104 with open(attach_path, "rb") as fh:105 writer.add_attachment(attach_path.name, fh.read())106 result = {"output": args.output, "attached": attach_path.name}107 108 with open(args.output, "wb") as fh:109 writer.write(fh)110 print(json.dumps(result, ensure_ascii=False))111 return 0112 113 114if __name__ == "__main__":115 sys.exit(main())116