Files
noteman-slicer/tests/test_editor.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

142 lines
4.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 import bundle # 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]
# The page fits the viewport once the window has a real size. show_page's
# own fit runs before layout, when the viewport is still its default.
editor.resize(900, 700)
editor.show()
app.processEvents()
scene = editor.view.sceneRect()
scale = editor.view.transform().m11()
viewport = editor.view.viewport()
fill = max(
scale * scene.width() / viewport.width(),
scale * scene.height() / viewport.height(),
)
# Fit means nearly touching one edge — Qt leaves a small margin of its own.
# A "≤ 1" check alone would pass a page zoomed down to a dot.
assert 0.9 <= fill <= 1.02, f"page is not fitted to the window: {fill:.3f}"
# The bundle is named after the song, not the PDF.
assert bundle.filename(project) == "Ketun-joululaulu.zip"
project.metadata["title"] = "AC/DC: T.N.T. (live)"
assert bundle.filename(project) == "ACDC-T.N.T.-live.zip"
project.metadata["title"] = "Ketun joululaulu"
editor.close()
source.close()
for f in (pdf, saved):
f.unlink()
tmp.rmdir()
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())