Add the editor: page view, cut editing, discard, levels, metadata
QGraphicsView for the viewport, with cuts and the content rectangle manipulated by hit-testing in the view rather than as movable items — the geometry is normalised, so what the screen shows and what the renderer uses are the same numbers at a different zoom. Cuts are edited as polylines: double-click adds one, drag moves it, Ctrl-click inserts a vertex, right-click deletes a vertex or the whole cut. That is how a straight cut becomes the stepped cut Engel needs. Slice boundaries are drawn, not just cut lines, so a trim anomaly is visible before export rather than after. Discarded slices are shaded. Pages preview at 1800px regardless of source resolution, cached per page, because re-reading a 4959x7017 vector render on every slider move is unusable. Autosave is debounced at 800ms and also fires on close. Closes #12 Closes #14 Closes #15 Closes #16 Closes #17 Closes #18 Closes #19 Closes #20 Closes #26
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""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.
|
||||
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) == project.content_rect
|
||||
|
||||
# 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())
|
||||
Reference in New Issue
Block a user