Files
noteman-slicer/noteman_slicer/cli.py
T
Esa Kataja 24b12214bb Fit the page on startup, name the bundle, show the selection
Three things a first session trips over.

The editor opened at an arbitrary zoom. show_page does fit the page, but
it runs before the window has been laid out, when the viewport is still
its default size; redo it once when the real size arrives.

The bundle was named after the PDF, which is whatever the download was
called. Take the song's title instead — unsafe characters dropped, then
whitespace collapsed to dashes. Accented letters stay, since ä and ö are
not a filesystem's problem, but a leading dot would hide the file.

And the selected slice was drawn as an outline whose top and bottom edges
run under the cut lines painted over them, leaving two thin verticals in
the margins and no way to tell what was selected. Wash the slice, as the
discard and engraved states already do, and keep the outline for the trim
anomalies it exists to show.
2026-07-29 13:09:27 +03:00

172 lines
6.2 KiB
Python
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.
"""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 _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)
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())