Saved state always wins over a fresh proposal, which is what the project file is for — but it also means a project made before detection proposed a content rectangle keeps the old whole-page one forever, and reopening or re-exporting changes nothing. --refit re-runs the proposal over every page while leaving cuts, discards, stepped cuts and metadata untouched. Verified on the real Engel project: rectangles updated on all 6 pages, all 4 cuts per page kept including both stepped ones, metadata intact, and exported slices scale larger now that the margin shadow no longer pads the width. --force remains the destructive option that re-detects everything.
181 lines
6.7 KiB
Python
181 lines
6.7 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 Project, default_path
|
||
|
||
source = open_source(args.pdf, SourceType(args.type) if args.type else None)
|
||
path = default_path(source.path)
|
||
|
||
if path.exists() and not args.force:
|
||
project = Project.load(path)
|
||
print(f"{path.name}: loaded")
|
||
if project.source_changed():
|
||
print(" WARNING: the PDF has changed since these cuts were made")
|
||
if args.refit:
|
||
# Re-propose the content rectangle without disturbing cuts,
|
||
# discards or metadata — for projects made before detection
|
||
# proposed one, or whose rectangle was dragged wrong.
|
||
for i in range(len(project.pages)):
|
||
gray = page_raster(source, i)
|
||
project.pages[i].content_rect = detect_page(gray).content
|
||
print(f" content rectangle re-fitted on {len(project.pages)} pages")
|
||
else:
|
||
detections, heights = [], []
|
||
for i in range(len(source)):
|
||
gray = page_raster(source, i)
|
||
detections.append(detect_page(gray))
|
||
heights.append(gray.shape[0])
|
||
project = Project.from_detection(source.path, detections, heights)
|
||
print(f"{path.name}: created from detection")
|
||
|
||
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 Project, default_path
|
||
|
||
source = open_source(args.pdf, SourceType(args.type) if args.type else None)
|
||
path = default_path(source.path)
|
||
|
||
if path.exists():
|
||
project = Project.load(path)
|
||
if project.source_changed():
|
||
print("WARNING: the PDF has changed since these cuts were made")
|
||
else:
|
||
detections, heights = [], []
|
||
for i in range(len(source)):
|
||
gray = page_raster(source, i)
|
||
detections.append(detect_page(gray))
|
||
heights.append(gray.shape[0])
|
||
project = Project.from_detection(source.path, detections, heights)
|
||
print("no project file; exporting straight from detection")
|
||
|
||
out = Path(args.out) if args.out else source.path.with_suffix(".zip")
|
||
bundle.write(project, source, out)
|
||
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)
|
||
|
||
|
||
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(
|
||
"--refit",
|
||
action="store_true",
|
||
help="re-propose the content rectangle, keeping cuts and metadata",
|
||
)
|
||
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("--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("--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())
|