Cropping of individual exos
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import pymupdf
|
||||
|
||||
from copienator.crop_exercise_bottoms import process_exercise_pdf
|
||||
|
||||
|
||||
class CropExerciseBottomsTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.review = self.root / "review"
|
||||
|
||||
def make_pdf(self, name: str, height: float, text_y: float, footer=True) -> Path:
|
||||
path = self.root / name
|
||||
with pymupdf.open() as document:
|
||||
page = document.new_page(width=600, height=height)
|
||||
page.insert_text((80, text_y), "student answer", fontsize=16)
|
||||
if footer:
|
||||
page.insert_text((20, height - 3), "Ex 2", fontsize=10)
|
||||
document.save(path)
|
||||
return path
|
||||
|
||||
def test_short_page_is_skipped(self):
|
||||
source = self.make_pdf("short.pdf", 200, 100)
|
||||
destination = self.root / "out" / source.name
|
||||
records = process_exercise_pdf(
|
||||
source, destination, self.review, 800, dpi=150
|
||||
)
|
||||
self.assertEqual(records[0]["status"], "skipped-short")
|
||||
self.assertFalse(destination.exists())
|
||||
|
||||
def test_footer_fragment_is_ignored_for_a_large_bottom_crop(self):
|
||||
source = self.make_pdf("large.pdf", 400, 100)
|
||||
destination = self.root / "out" / source.name
|
||||
records = process_exercise_pdf(
|
||||
source, destination, self.review, 800, dpi=150
|
||||
)
|
||||
self.assertEqual(records[0]["status"], "cropped")
|
||||
self.assertGreaterEqual(records[0]["bottom_removed_lines"], 4)
|
||||
with pymupdf.open(destination) as result:
|
||||
self.assertLess(result[0].rect.height, 250)
|
||||
self.assertAlmostEqual(result[0].rect.width, 600)
|
||||
|
||||
def test_crop_smaller_than_four_lines_is_not_written(self):
|
||||
source = self.make_pdf("small.pdf", 400, 330, footer=False)
|
||||
destination = self.root / "out" / source.name
|
||||
records = process_exercise_pdf(
|
||||
source, destination, self.review, 800, dpi=150
|
||||
)
|
||||
self.assertEqual(records[0]["status"], "unchanged-small-crop")
|
||||
self.assertFalse(destination.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,123 @@
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import pymupdf
|
||||
|
||||
from copienator.cli import CliError
|
||||
from copienator.commands import crop_exercise_bottoms
|
||||
from copienator.commands.clean import apply_cleanup, build_cleanup_plan
|
||||
from copienator.dispatcher import main
|
||||
from copienator.workspace import EvaluationWorkspace
|
||||
from copienator_gui.workflow import build_workflow
|
||||
|
||||
|
||||
class CropExerciseBottomsCommandTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.workspace = EvaluationWorkspace(Path(self.temp.name) / "Évaluation")
|
||||
self.workspace.copies_dir.mkdir(parents=True)
|
||||
self.copy_pdf = self.workspace.copies_dir / "Copie01.pdf"
|
||||
with pymupdf.open() as document:
|
||||
document.new_page(width=600, height=800)
|
||||
document.save(self.copy_pdf)
|
||||
self.answers = self.workspace.copies_dir / "Copie01"
|
||||
self.answers.mkdir()
|
||||
self.large = self.answers / "Ex 1.pdf"
|
||||
self.short = self.answers / "Ex 2.pdf"
|
||||
self._make_answer(self.large, 400, 100, footer=True)
|
||||
self._make_answer(self.short, 200, 100, footer=False)
|
||||
self.large_original = self.large.read_bytes()
|
||||
self.short_original = self.short.read_bytes()
|
||||
|
||||
@staticmethod
|
||||
def _make_answer(path: Path, height: float, text_y: float, *, footer: bool) -> None:
|
||||
with pymupdf.open() as document:
|
||||
page = document.new_page(width=600, height=height)
|
||||
page.insert_text((80, text_y), "student answer", fontsize=16)
|
||||
if footer:
|
||||
page.insert_text((20, height - 3), "Ex 2", fontsize=10)
|
||||
document.save(path)
|
||||
|
||||
def test_dispatcher_replaces_only_changed_answers_and_backs_them_up(self):
|
||||
with contextlib.redirect_stdout(io.StringIO()) as log:
|
||||
self.assertEqual(
|
||||
main(["crop-answer-bottoms", str(self.workspace.root), "--workers", "2"]),
|
||||
0,
|
||||
)
|
||||
self.assertIn("1 PDF remplacé", log.getvalue())
|
||||
with pymupdf.open(self.large) as cropped:
|
||||
self.assertLess(cropped[0].rect.height, 250)
|
||||
self.assertEqual(self.short.read_bytes(), self.short_original)
|
||||
backups = list(
|
||||
self.workspace.runs_dir.glob(
|
||||
"crop-exercise-bottoms-*/Copies/Copie01/Ex 1.pdf"
|
||||
)
|
||||
)
|
||||
self.assertEqual(len(backups), 1)
|
||||
self.assertEqual(backups[0].read_bytes(), self.large_original)
|
||||
self.assertFalse(
|
||||
list(
|
||||
self.workspace.runs_dir.glob(
|
||||
"crop-exercise-bottoms-*/Copies/Copie01/Ex 2.pdf"
|
||||
)
|
||||
)
|
||||
)
|
||||
self.assertTrue((backups[0].parents[2] / "report.json").is_file())
|
||||
|
||||
def test_detection_failure_does_not_publish_an_earlier_result(self):
|
||||
broken = self.answers / "Ex 3.pdf"
|
||||
broken.write_bytes(b"not a PDF")
|
||||
with contextlib.redirect_stdout(io.StringIO()), self.assertRaises(Exception):
|
||||
crop_exercise_bottoms.run(
|
||||
self.workspace, self.workspace.root, workers=1
|
||||
)
|
||||
self.assertEqual(self.large.read_bytes(), self.large_original)
|
||||
self.assertEqual(self.short.read_bytes(), self.short_original)
|
||||
self.assertEqual(broken.read_bytes(), b"not a PDF")
|
||||
self.assertFalse(self.workspace.runs_dir.exists())
|
||||
|
||||
def test_invalid_target_and_worker_count_are_rejected(self):
|
||||
with self.assertRaises(CliError):
|
||||
crop_exercise_bottoms.run(self.workspace, self.large, workers=1)
|
||||
with self.assertRaises(CliError):
|
||||
crop_exercise_bottoms.run(self.workspace, self.workspace.root, workers=0)
|
||||
|
||||
def test_archiving_removes_the_retained_originals_and_report(self):
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
crop_exercise_bottoms.run(
|
||||
self.workspace, self.workspace.root, workers=1
|
||||
)
|
||||
run_dirs = list(
|
||||
self.workspace.runs_dir.glob("crop-exercise-bottoms-*")
|
||||
)
|
||||
self.assertEqual(len(run_dirs), 1)
|
||||
self.workspace.correction_file.write_text("{}")
|
||||
student = self.workspace.return_dir / "Student"
|
||||
student.mkdir(parents=True)
|
||||
(student / "answer.jpg").write_bytes(b"return image")
|
||||
(student / "score.json").write_text("{}")
|
||||
plan = build_cleanup_plan(self.workspace)
|
||||
self.assertTrue(
|
||||
any(path.name == "report.json" for path in plan.deleted_files)
|
||||
)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
apply_cleanup(self.workspace, plan)
|
||||
self.assertFalse(self.workspace.runs_dir.exists())
|
||||
|
||||
def test_optional_step_follows_splitting_and_precedes_grouping(self):
|
||||
steps = build_workflow(False)
|
||||
index = next(
|
||||
i for i, step in enumerate(steps) if step.id == "crop_exercise_bottoms"
|
||||
)
|
||||
self.assertEqual(steps[index - 1].id, "splitting")
|
||||
self.assertEqual(steps[index + 1].id, "grouping")
|
||||
self.assertTrue(steps[index].optional)
|
||||
self.assertFalse(steps[index].auto_start_first_visit)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -68,6 +68,24 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.app._skip_step()
|
||||
self.assertEqual(self.app.state_store.step("crop_blank_margins")["status"], "skipped")
|
||||
|
||||
def test_optional_answer_bottom_crop_uses_parallel_workers_and_can_be_skipped(self):
|
||||
answers = self.evaluation / "Copies" / "Copie01"
|
||||
answers.mkdir(parents=True)
|
||||
(answers / "Ex 1.pdf").touch()
|
||||
self.app.tree.selection_set("crop_exercise_bottoms")
|
||||
self.app.update()
|
||||
self.assertEqual(self.app.current_step.id, "crop_exercise_bottoms")
|
||||
self.assertEqual(str(self.app.skip_button.cget("state")), "normal")
|
||||
command = self.app._make_command()
|
||||
self.assertEqual(
|
||||
command[-4:],
|
||||
["crop-answer-bottoms", self.app._evaluation_arg(), "--workers", "5"],
|
||||
)
|
||||
self.app._skip_step()
|
||||
self.assertEqual(
|
||||
self.app.state_store.step("crop_exercise_bottoms")["status"], "skipped"
|
||||
)
|
||||
|
||||
def test_redo_targets_selected_copy_and_copies_runnable_command(self):
|
||||
for folder in ("Copies", "Copies Originales"):
|
||||
(self.evaluation / folder).mkdir()
|
||||
|
||||
Reference in New Issue
Block a user