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.
142 lines
4.9 KiB
Python
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())
|