"""Command line entry point.""" from __future__ import annotations import argparse import sys import numpy as np from pathlib import Path from . import __version__, overlay from .detect import detect_page from .pdf import SourceType, open_source, page_raster def _info(args: argparse.Namespace) -> int: source = open_source(args.pdf, SourceType(args.type) if args.type else None) note = f" (detected {source.detected.value}, overridden)" if source.overridden else "" print(f"{source.path.name}: {source.type.value}{note}, {len(source)} pages") for i in range(len(source)): h, w = page_raster(source, i).shape print(f" p{i + 1:<3} {w}x{h}") source.close() return 0 def _detect(args: argparse.Namespace) -> int: source = open_source(args.pdf, SourceType(args.type) if args.type else None) out = Path(args.out) out.mkdir(parents=True, exist_ok=True) pages = range(len(source)) if args.page is None else [args.page - 1] for i in pages: gray = page_raster(source, i) detection = detect_page(gray) staves = [s.staff_height for s in detection.systems if s.staff_height] note = f", staff {np.median(staves):.0f}px" if staves else "" print( f"p{i + 1:<3} skew {detection.skew:+.2f}° " f"{len(detection.systems)} systems{note}" f"{' (no bracket)' if detection.bracketless else ''}" ) for n, system in enumerate(detection.systems, 1): print(f" sys{n}: {system.top}–{system.bottom} h={system.height}") overlay.write(gray, detection, out / f"{source.path.stem}-p{i + 1:02}.png") print(f"overlays written to {out}/") source.close() return 0 def _project(args: argparse.Namespace) -> int: from .project import default_path, open_project source = open_source(args.pdf, SourceType(args.type) if args.type else None) path = default_path(source.path) project = open_project(source, resume=args.resume and not args.force) if project.path is None: print(f"{path.name}: fresh session from detection") else: print(f"{path.name}: resumed") if project.source_changed(): print(" WARNING: the PDF has changed since these cuts were made") kept = project.kept_slices() for i, page in enumerate(project.pages): flags = "".join("." if d else "#" for d in page.discards) print(f" p{i + 1:<3} skew {page.skew:+.2f}° {page.slice_count} slices [{flags}]") print(f" {len(kept)} slices kept, {sum(p.slice_count for p in project.pages) - len(kept)} discarded") if args.save: print(f" saved to {project.save(path)}") source.close() return 0 def _export(args: argparse.Namespace) -> int: from . import bundle from .project import open_project source = open_source(args.pdf, SourceType(args.type) if args.type else None) project = open_project(source, resume=args.resume) if project.path is None: print("no unspent project state; exporting straight from detection") elif project.source_changed(): print("WARNING: the PDF has changed since these cuts were made") out = Path(args.out) if args.out else source.path.with_name(bundle.filename(project)) try: bundle.write(project, source, out) except ValueError as error: print(f"cannot export: {error}") print(" set one with: noteman-slicer edit … (Song → Title)") source.close() return 1 size = out.stat().st_size slices = len(project.kept_slices()) print(f"{out} {slices} slices, {size / 1024:.0f} KB ({size / max(slices, 1) / 1024:.1f} KB/slice)") source.close() return 0 def _open(args: argparse.Namespace) -> int: from . import bundle from .editor import launch try: project, pdf = bundle.read( Path(args.zip), Path(args.pdf) if args.pdf else None, force=args.force ) except (ValueError, KeyError) as error: print(f"cannot open: {error}") return 1 saved = project.save() print(f"{pdf.name}: {len(project.pages)} pages, {len(project.kept_slices())} slices") print(f" project written to {saved.name}") if args.no_edit: return 0 return launch(pdf, resume=True) def _edit(args: argparse.Namespace) -> int: from .editor import launch return launch( Path(args.pdf), SourceType(args.type) if args.type else None, resume=args.resume ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( prog="noteman-slicer", description="Cut score PDFs into noteman's slice images and markers.", ) parser.add_argument("--version", action="version", version=__version__) sub = parser.add_subparsers(dest="command", required=True) info = sub.add_parser("info", help="classify a PDF and report its page rasters") info.add_argument("pdf") info.add_argument( "--type", choices=[t.value for t in SourceType], help="override source-type detection", ) info.set_defaults(func=_info) det = sub.add_parser("detect", help="run detection and write debug overlays") det.add_argument("pdf") det.add_argument("--out", default="overlays", help="output directory") det.add_argument("--page", type=int, help="single 1-based page instead of all") det.add_argument("--type", choices=[t.value for t in SourceType]) det.set_defaults(func=_detect) proj = sub.add_parser("project", help="create or inspect the project file for a PDF") proj.add_argument("pdf") proj.add_argument("--save", action="store_true", help="write the project file") proj.add_argument("--force", action="store_true", help="re-detect, discarding existing state") proj.add_argument( "--resume", action="store_true", help="reopen an already-exported project instead of starting fresh", ) proj.add_argument("--type", choices=[t.value for t in SourceType]) proj.set_defaults(func=_project) exp = sub.add_parser("export", help="render the song and write a bundle") exp.add_argument("pdf") exp.add_argument("--out", help="output zip (default: alongside the PDF)") exp.add_argument("--resume", action="store_true", help="use already-exported project state") exp.add_argument("--type", choices=[t.value for t in SourceType]) exp.set_defaults(func=_export) opn = sub.add_parser("open", help="unpack a bundle back into an editable project") opn.add_argument("zip") opn.add_argument("--pdf", help="where to write the archived PDF (default: beside the bundle)") opn.add_argument( "--no-edit", action="store_true", help="write the project and stop, without the editor" ) opn.add_argument( "--force", action="store_true", help="overwrite an existing PDF or project file" ) opn.set_defaults(func=_open) ed = sub.add_parser("edit", help="open the editor") ed.add_argument("pdf") ed.add_argument( "--resume", action="store_true", help="reopen an already-exported project instead of starting fresh", ) ed.add_argument("--type", choices=[t.value for t in SourceType]) ed.set_defaults(func=_edit) args = parser.parse_args(argv) return args.func(args) if __name__ == "__main__": sys.exit(main())