Files
noteman-slicer/tests/test_editor.py
T
Esa Kataja b6847a06ee Treat a project as spent once its song has been exported
Opening an exported song starts a fresh session from detection instead
of resuming: cuts, discards and metadata do not carry over, so a re-cut
never inherits decisions that have already shipped. --resume overrides
it on edit, export and project.

This reverses what was agreed in planning and written into docs/spec.md
and CONTEXT.md, which promised resume-across-sessions and re-export.
Both are corrected. The cost is deliberate and worth stating: changing
the width cap or adding the SVG renderer later now means re-cutting each
song by hand rather than regenerating every bundle from its project
file.

Export records the flag in bundle.write, so no caller can forget it.

Also removed --refit and the Auto-fit buttons, which were added without
being asked for and whose only purpose - migrating projects made before
the content rectangle was proposed - disappears once exported projects
start fresh. Reset now restores detection's proposal rather than the
whole page: clearing to full width would undo the thing the rectangle
exists for, so one button covers it.

open_project() replaces four copies of load-or-detect across the CLI
and the editor.
2026-07-28 23:49:03 +03:00

119 lines
3.9 KiB
Python

"""Runnable check that the editor builds and its edits reach project state.
Runs offscreen, so it verifies wiring rather than appearance: that the widgets
construct, that an edit changes the model, and that autosave and export work.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
import numpy as np # noqa: E402
import pymupdf # noqa: E402
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from PySide6.QtWidgets import QApplication # noqa: E402
from noteman_slicer.detect import detect_page # noqa: E402
from noteman_slicer.editor import Editor # noqa: E402
from noteman_slicer.pdf import open_source, page_raster # noqa: E402
from noteman_slicer.project import Cut, Project, default_path # noqa: E402
W, H = 1200, 1600
def _scan_pdf(path: Path) -> None:
art = np.full((H, W), 255, np.uint8)
for top in (300, 800):
art[top : top + 200, 100:104] = 0
for staff in (top, top + 140):
for i in range(5):
art[staff + i * 15 : staff + i * 15 + 2, 110:1100] = 0
pix = pymupdf.Pixmap(pymupdf.csGRAY, W, H, bytearray(art.tobytes()), False)
doc = pymupdf.open()
doc.new_page(width=595, height=842).insert_image(pymupdf.Rect(0, 0, 595, 842), pixmap=pix)
doc.save(path)
def main() -> int:
tmp = Path(__file__).with_name("_tmp")
tmp.mkdir(exist_ok=True)
pdf = tmp / "scan.pdf"
_scan_pdf(pdf)
app = QApplication.instance() or QApplication(sys.argv[:1])
source = open_source(pdf)
gray = page_raster(source, 0)
project = Project.from_detection(pdf, [detect_page(gray)], [gray.shape[0]])
editor = Editor(source, project)
page = project.pages[0]
# Cuts.
before = page.slice_count
editor.view.selected_cut = page.add_cut(Cut.straight(0.5))
editor.view.redraw()
assert page.slice_count == before + 1
# A vertex turns a straight cut into a stepped one.
cut = page.cuts[editor.view.selected_cut]
cut.points.insert(1, (0.4, cut.y_at(0.4)))
cut.points[1] = (0.4, cut.points[1][1] + 0.03)
assert cut.straight_y is None, "the cut should no longer be straight"
editor.view.redraw()
# Discard.
editor.view.selected_slice = 1
was = page.discards[1]
editor.view.toggle_discard()
assert page.discards[1] != was
# Skew and levels reach the model and re-render without raising.
editor.skew.setValue(-1.4)
assert abs(page.skew + 1.4) < 1e-6
editor.black.setValue(40)
editor.white.setValue(210)
assert project.page_levels(0) == (40, 210)
# Metadata.
editor.metadata["title"].setText("Ketun joululaulu")
editor.metadata["composer"].setText("trad.")
assert project.metadata["title"] == "Ketun joululaulu"
# Content rectangle edits, and reset going back to detection's proposal for
# the page as it now stands — not to the whole page, which would undo the
# thing the rectangle exists for.
expected = detect_page(editor._preview(0), skew=0.0).content
page.content_rect = (0.05, 0.02, 0.95, 0.98)
assert project.page_content_rect(0) == (0.05, 0.02, 0.95, 0.98)
editor._reset_rect()
assert project.page_content_rect(0) == expected, (project.page_content_rect(0), expected)
assert project.page_content_rect(0) != (0.0, 0.0, 1.0, 1.0)
# Autosave target, then a round-trip through disk.
editor._save()
saved = default_path(pdf)
assert saved.exists()
reloaded = Project.load(saved)
assert reloaded.metadata["title"] == "Ketun joululaulu"
assert reloaded.pages[0].skew == -1.4
assert reloaded.pages[0].levels == (40, 210)
assert [c.points for c in reloaded.pages[0].cuts] == [c.points for c in page.cuts]
editor.close()
source.close()
for f in (pdf, saved):
f.unlink()
tmp.rmdir()
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())