Files
Copies/tests/test_manual_cut.py
T

126 lines
7.0 KiB
Python

import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import numpy as np
import pymupdf
from copienator import CliError, EvaluationWorkspace, atomic_write_json, read_json
from copienator.commands.resolve_manual import parse_instruction_text, resolve_manual
from copienator.pdf_cut import cut_position, split_pdf
from copienator_gui.cut_helper import PAGE_GAP, cut_operator, percentage_at_y, y_at_percentage
def make_pdf(path, heights=(300, 700), rotation=0, cropped=False):
with pymupdf.open() as document:
for index, height in enumerate(heights):
page = document.new_page(width=200, height=height)
page.draw_rect(pymupdf.Rect(0, 0, 200, height / 2), color=None, fill=(1, 0, 0))
page.draw_rect(pymupdf.Rect(0, height / 2, 200, height), color=None, fill=(0, 0, 1))
page.insert_text((30, 30), f"Page {index + 1}")
if cropped:
page.set_cropbox(pymupdf.Rect(10, 20, 180, height - 30))
page.set_rotation(rotation)
document.save(path)
class ManualCutTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def test_parser_accepts_new_operators_and_rejects_invalid_cuts(self):
for keep in (1, 2):
for action in (">", "x"):
item = parse_instruction_text(f"Copie16 Ex 4 : 1) c{{43.125}}{keep}{action} |Ex 4 : 2)")[0]
self.assertEqual(item.cut, (43.125, keep))
self.assertEqual(item.should_merge, action == ">")
self.assertTrue(item.pipe_first)
for operator in ("c{0}1>", "c{100}1>", "c{-1}1>", "c{101}1>", "c{43}3>",
"c{43}1s", "c{NaN}1>"):
with self.assertRaises(CliError):
parse_instruction_text(f"Copie16 A {operator} B")
with self.assertRaises(CliError):
parse_instruction_text("Copie16 A c{43}1> A")
def test_page_boundary_preserves_whole_pages_without_clipping(self):
source, first, second = [self.root / name for name in ("source.pdf", "first.pdf", "second.pdf")]
make_pdf(source, heights=(100, 200), rotation=180)
before = source.read_bytes()
with patch("pymupdf.Page.show_pdf_page", side_effect=AssertionError("must not clip whole pages")):
split_pdf(source, 33.333333, first, second)
for path, height in ((first, 100), (second, 200)):
with pymupdf.open(path) as pdf:
self.assertEqual(len(pdf), 1)
self.assertEqual(pdf[0].rect.height, height)
self.assertEqual(pdf[0].rotation, 180)
self.assertEqual(source.read_bytes(), before)
def test_in_page_cut_preserves_visible_pixels_for_cropped_rotated_pages(self):
source, first, second = [self.root / name for name in ("source.pdf", "first.pdf", "second.pdf")]
for rotation in (0, 90, 180, 270):
with self.subTest(rotation=rotation):
make_pdf(source, heights=(400,), rotation=rotation, cropped=True)
with pymupdf.open(source) as pdf:
pix = pdf[0].get_pixmap()
expected = np.frombuffer(pix.samples, np.uint8).reshape(pix.height, pix.width, 3)
split_pdf(source, 50, first, second)
for path, pixels in ((first, expected[:len(expected)//2]), (second, expected[len(expected)//2:])):
with pymupdf.open(path) as pdf:
pix = pdf[0].get_pixmap()
actual = np.frombuffer(pix.samples, np.uint8).reshape(pix.height, pix.width, 3)
self.assertEqual(actual.shape, pixels.shape)
self.assertLess(np.abs(actual.astype(float) - pixels).mean(), 0.1)
def test_cut_within_page_preserves_subsequent_pages(self):
source, first, second = [self.root / name for name in ("source.pdf", "first.pdf", "second.pdf")]
make_pdf(source)
split_pdf(source, 15, first, second)
with pymupdf.open(first) as pdf:
self.assertEqual([page.rect.height for page in pdf], [150])
with pymupdf.open(second) as pdf:
self.assertEqual([page.rect.height for page in pdf], [150, 700])
def test_resolver_archives_source_and_recorrrects_both_labels(self):
for keep in (1, 2):
for mode in (">", "x"):
for pipe_first in (False, True):
with self.subTest(keep=keep, mode=mode, pipe_first=pipe_first):
root = self.root / f"{keep}{mode == '>'}{pipe_first}"
copies = root / "Copies" / "Copie16"
copies.mkdir(parents=True)
source, target = copies / "A.pdf", copies / "B.pdf"
make_pdf(source)
make_pdf(target, heights=(80,))
original, destination = source.read_bytes(), target.read_bytes()
atomic_write_json(root / "correction.json", {
label: [[{"id": "16", "result": {}}]] for label in ("A", "B")
})
new_label = "|B" if pipe_first else "B|"
(root / "manual_resolutions.txt").write_text(f"Copie16 A c{{30}}{keep}{mode} {new_label}\n")
self.assertEqual(resolve_manual(EvaluationWorkspace(root)), 0)
self.assertEqual((copies / "A_old.pdf").read_bytes(), original)
self.assertEqual((copies / "B_old.pdf").read_bytes(), destination)
retained, moved = (300, 700) if keep == 1 else (700, 300)
with pymupdf.open(copies / "A_new.pdf") as pdf:
self.assertEqual([page.rect.height for page in pdf], [retained])
with pymupdf.open(copies / "B_new.pdf") as pdf:
expected = ([moved, 80] if pipe_first else [80, moved]) if mode == ">" else [moved]
self.assertEqual([page.rect.height for page in pdf], expected)
self.assertEqual(read_json(root / "refaire.json"), [["Copie16", ["A", "B"]]])
self.assertTrue(all(read_json(root / "correction.json")[label][0][0]["result"]["suffix"] == "_new" for label in ("A", "B")))
self.assertFalse(list(copies.glob("temp_*.pdf")))
def test_helper_snaps_to_gap_and_round_trips_percentage(self):
heights = [100, 200]
for y in (95, 100, 100 + PAGE_GAP / 2, 100 + PAGE_GAP + 5):
percent = percentage_at_y(y, heights, 1)
self.assertEqual(cut_position(heights, percent), (1, 0))
self.assertEqual(y_at_percentage(percent, heights, 1), 100 + PAGE_GAP / 2)
self.assertEqual(cut_operator(43, 1, ">"), "c{43}1>")
self.assertEqual(cut_operator(100/3, 2, "x"), "c{33.333333}2x")
self.assertEqual(cut_position(heights, 33.333333), (1, 0))
self.assertNotEqual(cut_position(heights, 33.3)[1], 0)