cropping summaries
This commit is contained in:
@@ -134,6 +134,27 @@ def _publish(
|
|||||||
return backup_root
|
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:
|
def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> ExitCode:
|
||||||
if workers < 1:
|
if workers < 1:
|
||||||
raise CliError(
|
raise CliError(
|
||||||
@@ -161,15 +182,23 @@ def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> Ex
|
|||||||
raise CliError(
|
raise CliError(
|
||||||
f"{source} a changé pendant l’analyse. Aucun PDF remplacé."
|
f"{source} a changé pendant l’analyse. Aucun PDF remplacé."
|
||||||
)
|
)
|
||||||
|
cropped_exercises, mean_percentage = crop_statistics(records)
|
||||||
if not changed_files:
|
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
|
return ExitCode.SUCCESS
|
||||||
backup = _publish(workspace, changed_files, staging, records)
|
backup = _publish(workspace, changed_files, staging, records)
|
||||||
cropped_pages = sum(record["status"] == "cropped" for record in records)
|
cropped_pages = sum(record["status"] == "cropped" for record in records)
|
||||||
print(f"Sauvegarde des PDF non rognés : {backup}", flush=True)
|
print(f"Sauvegarde des PDF non rognés : {backup}", flush=True)
|
||||||
print(
|
print(
|
||||||
f"Terminé : {cropped_pages} page(s) rognée(s) dans "
|
f"Terminé : {cropped_exercises} exercice(s) rogné(s), soit "
|
||||||
f"{len(changed_files)} PDF remplacé(s).",
|
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,
|
flush=True,
|
||||||
)
|
)
|
||||||
return ExitCode.SUCCESS
|
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"]))
|
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:
|
def run(workspace: EvaluationWorkspace, target: Path, *, workers: int = 5) -> ExitCode:
|
||||||
if workers < 1:
|
if workers < 1:
|
||||||
raise CliError("Le nombre de traitements parallèles doit être positif.",
|
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),
|
(backup/"report.json").write_text(json.dumps(records, ensure_ascii=False, indent=2),
|
||||||
encoding="utf-8")
|
encoding="utf-8")
|
||||||
print(f"Sauvegarde des PDF non rognés : {originals}", flush=True)
|
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 ; "
|
print(f"Terminé : {cropped}/{len(records)} pages rognées ; "
|
||||||
f"{len(files)} PDF remplacés dans Copies.", flush=True)
|
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
|
return ExitCode.SUCCESS
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class CropExerciseBottomsCommandTests(unittest.TestCase):
|
|||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
self.assertIn("1 PDF remplacé", log.getvalue())
|
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:
|
with pymupdf.open(self.large) as cropped:
|
||||||
self.assertLess(cropped[0].rect.height, 250)
|
self.assertLess(cropped[0].rect.height, 250)
|
||||||
self.assertEqual(self.short.read_bytes(), self.short_original)
|
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())
|
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):
|
def test_detection_failure_does_not_publish_an_earlier_result(self):
|
||||||
broken = self.answers / "Ex 3.pdf"
|
broken = self.answers / "Ex 3.pdf"
|
||||||
broken.write_bytes(b"not a PDF")
|
broken.write_bytes(b"not a PDF")
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class CropMarginsCommandTests(unittest.TestCase):
|
|||||||
with contextlib.redirect_stdout(io.StringIO()) as log:
|
with contextlib.redirect_stdout(io.StringIO()) as log:
|
||||||
self.assertEqual(main(["crop-margins", str(self.workspace.root)]), 0)
|
self.assertEqual(main(["crop-margins", str(self.workspace.root)]), 0)
|
||||||
self.assertIn("Page 2/2", log.getvalue())
|
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:
|
with pymupdf.open(self.source) as result:
|
||||||
self.assertEqual(len(result), 2)
|
self.assertEqual(len(result), 2)
|
||||||
self.assertLess(result[0].rect.height, 150)
|
self.assertLess(result[0].rect.height, 150)
|
||||||
@@ -50,6 +53,32 @@ class CropMarginsCommandTests(unittest.TestCase):
|
|||||||
self.assertEqual(backups[0].read_bytes(), self.original)
|
self.assertEqual(backups[0].read_bytes(), self.original)
|
||||||
self.assertTrue((backups[0].parent.parent/"report.json").is_file())
|
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):
|
def test_failure_or_interruption_never_publishes_partial_batch(self):
|
||||||
second = self.workspace.copies_dir/"Copie02.pdf"
|
second = self.workspace.copies_dir/"Copie02.pdf"
|
||||||
second.write_bytes(self.original)
|
second.write_bytes(self.original)
|
||||||
|
|||||||
Reference in New Issue
Block a user