cropping summaries

This commit is contained in:
2026-09-10 20:03:23 +02:00
parent dfce79f240
commit 060859ddef
4 changed files with 120 additions and 4 deletions
+32 -3
View File
@@ -134,6 +134,27 @@ def _publish(
return backup_root
def crop_statistics(records: list[dict]) -> tuple[int, float]:
"""Return cropped exercise count and mean removed percentage per exercise."""
totals: dict[str, list[float]] = {}
cropped_files: set[str] = set()
for record in records:
total_height, removed_height = totals.setdefault(record["file"], [0.0, 0.0])
totals[record["file"]] = [
total_height + float(record["height_lines"]),
removed_height + float(record["bottom_removed_lines"]),
]
if record["status"] == "cropped":
cropped_files.add(record["file"])
percentages = [
min(100.0, totals[file_name][1] / totals[file_name][0] * 100)
for file_name in cropped_files
if totals[file_name][0] > 0
]
mean_percentage = sum(percentages) / len(percentages) if percentages else 0.0
return len(cropped_files), mean_percentage
def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> ExitCode:
if workers < 1:
raise CliError(
@@ -161,15 +182,23 @@ def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> Ex
raise CliError(
f"{source} a changé pendant lanalyse. Aucun PDF remplacé."
)
cropped_exercises, mean_percentage = crop_statistics(records)
if not changed_files:
print("Terminé : aucun PDF ne remplit les critères de rognage.", flush=True)
print("Terminé : aucun exercice ne remplit les critères de rognage.", flush=True)
print(
"Rognage moyen des exercices modifiés : 0.0 %.", flush=True
)
return ExitCode.SUCCESS
backup = _publish(workspace, changed_files, staging, records)
cropped_pages = sum(record["status"] == "cropped" for record in records)
print(f"Sauvegarde des PDF non rognés : {backup}", flush=True)
print(
f"Terminé : {cropped_pages} page(s) rognée(s) dans "
f"{len(changed_files)} PDF remplacé(s).",
f"Terminé : {cropped_exercises} exercice(s) rogné(s), soit "
f"{cropped_pages} page(s) dans {len(changed_files)} PDF remplacé(s).",
flush=True,
)
print(
f"Rognage moyen des exercices modifiés : {mean_percentage:.1f} %.",
flush=True,
)
return ExitCode.SUCCESS
+26 -1
View File
@@ -83,6 +83,26 @@ def process_copies(files: list[Path], staging: Path, workers: int) -> list[dict]
key=lambda row: (order[row["file"]], row["page"]))
def crop_statistics(records: list[dict]) -> tuple[int, float, int]:
"""Return cropped page count, their mean removed percentage, and >30% count."""
percentages: list[float] = []
for record in records:
removed_mm = record["top_removed_mm"] + record["bottom_removed_mm"]
if removed_mm <= 0:
continue
x0, y0, x1, y1 = record["original_cropbox"]
original_height_points = (
x1 - x0 if record.get("rotation", 0) % 180 else y1 - y0
)
if original_height_points <= 0:
continue
original_height_mm = original_height_points * 25.4 / 72
percentages.append(min(100.0, removed_mm / original_height_mm * 100))
mean_percentage = sum(percentages) / len(percentages) if percentages else 0.0
over_thirty = sum(percentage > 30 for percentage in percentages)
return len(percentages), mean_percentage, over_thirty
def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> ExitCode:
if workers < 1:
raise CliError("Le nombre de traitements parallèles doit être positif.",
@@ -109,9 +129,14 @@ def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> Ex
(backup/"report.json").write_text(json.dumps(records, ensure_ascii=False, indent=2),
encoding="utf-8")
print(f"Sauvegarde des PDF non rognés : {originals}", flush=True)
cropped = sum(row["top_removed_mm"]+row["bottom_removed_mm"] > 0 for row in records)
cropped, mean_percentage, over_thirty = crop_statistics(records)
print(f"Terminé : {cropped}/{len(records)} pages rognées ; "
f"{len(files)} PDF remplacés dans Copies.", flush=True)
print(
f"Rognage moyen des pages modifiées : {mean_percentage:.1f} % ; "
f"{over_thirty} page(s) rognée(s) de plus de 30 %.",
flush=True,
)
return ExitCode.SUCCESS
@@ -49,6 +49,8 @@ class CropExerciseBottomsCommandTests(unittest.TestCase):
0,
)
self.assertIn("1 PDF remplacé", log.getvalue())
self.assertIn("1 exercice(s) rogné(s)", log.getvalue())
self.assertIn("Rognage moyen des exercices modifiés", 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)
@@ -68,6 +70,37 @@ class CropExerciseBottomsCommandTests(unittest.TestCase):
)
self.assertTrue((backups[0].parents[2] / "report.json").is_file())
def test_crop_statistics_average_percentages_by_exercise(self):
records = [
{
"file": "Copies/Copie01/Ex 1.pdf",
"height_lines": 10.0,
"bottom_removed_lines": 4.0,
"status": "cropped",
},
{
"file": "Copies/Copie01/Ex 1.pdf",
"height_lines": 10.0,
"bottom_removed_lines": 0.0,
"status": "skipped-short",
},
{
"file": "Copies/Copie01/Ex 2.pdf",
"height_lines": 10.0,
"bottom_removed_lines": 5.0,
"status": "cropped",
},
{
"file": "Copies/Copie01/Ex 3.pdf",
"height_lines": 10.0,
"bottom_removed_lines": 0.0,
"status": "unchanged-small-crop",
},
]
count, average = crop_exercise_bottoms.crop_statistics(records)
self.assertEqual(count, 2)
self.assertAlmostEqual(average, 35.0)
def test_detection_failure_does_not_publish_an_earlier_result(self):
broken = self.answers / "Ex 3.pdf"
broken.write_bytes(b"not a PDF")
+29
View File
@@ -37,6 +37,9 @@ class CropMarginsCommandTests(unittest.TestCase):
with contextlib.redirect_stdout(io.StringIO()) as log:
self.assertEqual(main(["crop-margins", str(self.workspace.root)]), 0)
self.assertIn("Page 2/2", log.getvalue())
self.assertIn("1/2 pages rognées", log.getvalue())
self.assertIn("Rognage moyen des pages modifiées", log.getvalue())
self.assertIn("page(s) rognée(s) de plus de 30 %", log.getvalue())
with pymupdf.open(self.source) as result:
self.assertEqual(len(result), 2)
self.assertLess(result[0].rect.height, 150)
@@ -50,6 +53,32 @@ class CropMarginsCommandTests(unittest.TestCase):
self.assertEqual(backups[0].read_bytes(), self.original)
self.assertTrue((backups[0].parent.parent/"report.json").is_file())
def test_crop_statistics_use_only_modified_pages(self):
records = [
{
"top_removed_mm": 10.0,
"bottom_removed_mm": 20.0,
"original_cropbox": [0, 0, 200, 300],
"rotation": 0,
},
{
"top_removed_mm": 40.0,
"bottom_removed_mm": 0.0,
"original_cropbox": [0, 0, 200, 300],
"rotation": 90,
},
{
"top_removed_mm": 0.0,
"bottom_removed_mm": 0.0,
"original_cropbox": [0, 0, 200, 300],
"rotation": 0,
},
]
count, average, over_thirty = crop_margins.crop_statistics(records)
self.assertEqual(count, 2)
self.assertAlmostEqual(average, (28.3465 + 56.6929) / 2, places=3)
self.assertEqual(over_thirty, 1)
def test_failure_or_interruption_never_publishes_partial_batch(self):
second = self.workspace.copies_dir/"Copie02.pdf"
second.write_bytes(self.original)