"""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 # The levels bar carries the page's own histogram and drives the model # directly — there are no sliders behind it to keep in step. assert editor.levels.hist.sum() > 0, "the histogram should hold the scan" editor.levels.set_levels(40, 210) editor._levels_changed(40, 210) assert project.page_levels(0) == (40, 210) editor.rail.picked.emit(0) # the page rail navigates assert editor.index == 0 and len(editor.rail.buttons) == len(project.pages) # 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())