cropping summaries
This commit is contained in:
@@ -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 l’analyse. 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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user