Add project state with cuts, discards and atomic save
Everything the human decides, in normalised page coordinates so the file is independent of DPI and of which renderer produced it. The bundle will be generated from this, which is what makes re-export possible without repeating human work. Cuts are polylines from the start, two points being the ordinary straight case, so the stepped cuts Engel needs are a data question rather than a migration. Adding a cut splits a slice and copies its discard flag to both halves; removing one merges them. Boundary cuts belong here rather than in detection: detection emits cuts only between systems, so a page would have exactly as many slices as systems, with the header and footer inside the first and last. Isolating and discarding them is a slicing decision. Saves are write-then-rename, so a crash mid-save cannot destroy the previous state. The PDF is hashed, not copied, so an edit underneath is reported rather than silently re-cut. Ketun joululaulu now yields 24 kept slices over 12 pages and Feliz Navidad 20 over 4, with headers and footers discarded on every page. Closes #10, #11, #13
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Runnable check for project state: round-trip, cut edits, discard pre-set."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from noteman_slicer.detect import PageDetection, System # noqa: E402
|
||||
from noteman_slicer.project import Cut, Project, default_path # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tmp = Path(__file__).with_name("_tmp")
|
||||
tmp.mkdir(exist_ok=True)
|
||||
pdf = tmp / "song.pdf"
|
||||
pdf.write_bytes(b"%PDF-1.7 not really a pdf, only its bytes are hashed")
|
||||
|
||||
height = 1000
|
||||
detection = PageDetection(
|
||||
skew=-1.1,
|
||||
systems=[System(200, 400, 60.0), System(600, 800, 60.0)],
|
||||
cuts=[500],
|
||||
)
|
||||
project = Project.from_detection(pdf, [detection], [height])
|
||||
page = project.pages[0]
|
||||
|
||||
# One cut between the systems, plus a boundary cut above the first and
|
||||
# below the last — so the header and footer become their own slices.
|
||||
assert len(page.cuts) == 3, [c.points for c in page.cuts]
|
||||
assert page.discards == [True, False, False, True], page.discards
|
||||
assert page.slice_count == 4
|
||||
assert project.kept_slices() == [(0, 1), (0, 2)], project.kept_slices()
|
||||
|
||||
# Geometry is normalised, so it survives any change of resolution.
|
||||
assert all(0.0 <= y <= 1.0 for cut in page.cuts for _, y in cut.points)
|
||||
assert page.cuts[1].straight_y == 0.5
|
||||
|
||||
# A straight cut is flat; a stepped one is not, and interpolates.
|
||||
step = Cut([(0.0, 0.20), (0.35, 0.20), (0.35, 0.40), (1.0, 0.40)])
|
||||
assert step.straight_y is None
|
||||
assert step.y_at(0.0) == 0.20
|
||||
assert step.y_at(1.0) == 0.40
|
||||
assert step.y_at(0.35) == 0.20 or step.y_at(0.35) == 0.40
|
||||
assert step.highest == 0.20 and step.lowest == 0.40
|
||||
|
||||
# Adding a cut splits a slice and keeps that slice's flag on both halves.
|
||||
before = page.slice_count
|
||||
index = page.add_cut(Cut.straight(0.65))
|
||||
assert page.slice_count == before + 1
|
||||
assert index == 2, index
|
||||
assert page.discards == [True, False, False, False, True], page.discards
|
||||
|
||||
# Removing it merges them again.
|
||||
page.remove_cut(index)
|
||||
assert page.slice_count == before
|
||||
assert page.discards == [True, False, False, True], page.discards
|
||||
|
||||
# Round-trip.
|
||||
saved = project.save()
|
||||
assert saved == default_path(pdf), saved
|
||||
reloaded = Project.load(saved)
|
||||
assert reloaded.pages[0].skew == -1.1
|
||||
assert reloaded.pages[0].discards == page.discards
|
||||
assert [c.points for c in reloaded.pages[0].cuts] == [c.points for c in page.cuts]
|
||||
assert reloaded.source_hash == project.source_hash
|
||||
assert not reloaded.source_changed()
|
||||
|
||||
# A PDF edited underneath must be reported, not silently re-cut.
|
||||
pdf.write_bytes(b"%PDF-1.7 different bytes entirely")
|
||||
assert reloaded.source_changed()
|
||||
|
||||
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