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