diff --git a/step-0/check_pdf.py b/step-0/check_pdf.py new file mode 100644 index 0000000..2bee9b8 --- /dev/null +++ b/step-0/check_pdf.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Step 0 — Verifica idoneità PDF + +Legge tutti i PDF in sources/ e salva un report per ognuno in step-0/. + +Uso: + python step-0/check_pdf.py + +Output: + step-0/_step0_report.txt +""" + +import sys +import statistics +from datetime import datetime +from pathlib import Path + + +def check_pdf(pdf_path: str, save: bool = True) -> None: + try: + import pdfplumber + except ImportError: + print("Errore: pdfplumber non è installato.") + print(" pip install pdfplumber") + sys.exit(1) + + path = Path(pdf_path) + if not path.exists(): + print(f"Errore: file non trovato — {pdf_path}") + sys.exit(1) + if path.suffix.lower() != ".pdf": + print(f"Errore: il file non è un PDF — {pdf_path}") + sys.exit(1) + + lines = [] # righe del report + results = [] # (etichetta, stato, messaggio) + + def out(text=""): + lines.append(text) + print(text) + + out(f"Step 0 — Verifica idoneità PDF") + out(f"File: {path.name}") + out(f"Data: {datetime.now().strftime('%Y-%m-%d %H:%M')}") + out("=" * 50) + + # ------------------------------------------------------------------ # + # Criterio 1 — Non protetto da password + # ------------------------------------------------------------------ # + try: + with pdfplumber.open(path) as pdf: + n_pages = len(pdf.pages) + results.append(("Non protetto da password", "PASS", f"{n_pages} pagine")) + except Exception as e: + msg = str(e).lower() + if "password" in msg or "encrypted" in msg or "decrypt" in msg: + results.append(("Non protetto da password", "FAIL", + "Il PDF è cifrato — non può essere elaborato")) + else: + results.append(("Non protetto da password", "FAIL", + f"Impossibile aprire il file: {e}")) + _render_results(results, out) + _maybe_save(lines, path, save) + return + + # ------------------------------------------------------------------ # + # Lettura pagine — una sola passata + # ------------------------------------------------------------------ # + char_counts = [] + line_lengths = [] + all_text = "" + empty_pages = 0 + + with pdfplumber.open(path) as pdf: + for page in pdf.pages: + text = page.extract_text() or "" + all_text += text + "\n" + chars = len(text.strip()) + char_counts.append(chars) + if chars == 0: + empty_pages += 1 + for line in text.splitlines(): + stripped = line.strip() + if stripped: + line_lengths.append(len(stripped)) + + total_pages = len(char_counts) + pages_with_text = sum(1 for c in char_counts if c > 50) + text_coverage = pages_with_text / total_pages if total_pages > 0 else 0 + + # ------------------------------------------------------------------ # + # Criterio 2 — Testo estraibile + # ------------------------------------------------------------------ # + if text_coverage >= 0.7: + results.append(("Testo estraibile", "PASS", + f"{pages_with_text}/{total_pages} pagine con testo ({text_coverage:.0%})")) + elif text_coverage >= 0.4: + results.append(("Testo estraibile", "WARN", + f"Solo {pages_with_text}/{total_pages} pagine con testo — revisione estesa necessaria")) + else: + results.append(("Testo estraibile", "FAIL", + f"Solo {pages_with_text}/{total_pages} pagine con testo — probabilmente scansionato")) + + # ------------------------------------------------------------------ # + # Criterio 3 — Generato digitalmente (non scansionato) + # ------------------------------------------------------------------ # + pages_text_only = [c for c in char_counts if c > 0] + avg_chars = statistics.mean(pages_text_only) if pages_text_only else 0 + + if avg_chars >= 300: + results.append(("Generato digitalmente (non scansionato)", "PASS", + f"Media {avg_chars:.0f} char/pagina")) + elif avg_chars >= 100: + results.append(("Generato digitalmente (non scansionato)", "WARN", + f"Media bassa: {avg_chars:.0f} char/pagina — alcune pagine potrebbero essere immagini")) + else: + results.append(("Generato digitalmente (non scansionato)", "FAIL", + f"Media molto bassa: {avg_chars:.0f} char/pagina — il PDF sembra scansionato")) + + # ------------------------------------------------------------------ # + # Criterio 4 — Pagine vuote + # ------------------------------------------------------------------ # + if empty_pages == 0: + results.append(("Pagine vuote", "PASS", "Nessuna pagina vuota")) + elif empty_pages <= total_pages * 0.05: + results.append(("Pagine vuote", "WARN", + f"{empty_pages} pagine vuote (≤ 5%) — probabilmente copertine o separatori")) + else: + results.append(("Pagine vuote", "WARN", + f"{empty_pages} pagine vuote ({empty_pages/total_pages:.0%}) — controllare")) + + # ------------------------------------------------------------------ # + # Criterio desiderabile — Layout a colonne singola + # ------------------------------------------------------------------ # + if line_lengths: + median_len = statistics.median(line_lengths) + short_lines = sum(1 for l in line_lengths if l < median_len * 0.4) + short_ratio = short_lines / len(line_lengths) + if short_ratio < 0.15: + results.append(("Layout a colonne singola (desiderabile)", "PASS", + f"Righe corte: {short_ratio:.0%} — struttura lineare")) + elif short_ratio < 0.35: + results.append(("Layout a colonne singola (desiderabile)", "WARN", + f"Righe corte: {short_ratio:.0%} — possibile layout a colonne parziale")) + else: + results.append(("Layout a colonne singola (desiderabile)", "WARN", + f"Righe corte: {short_ratio:.0%} — probabile layout a colonne multiple")) + else: + results.append(("Layout a colonne singola (desiderabile)", "WARN", + "Impossibile analizzare (nessuna riga estratta)")) + + # ------------------------------------------------------------------ # + # Criterio desiderabile — Struttura logica (titoli) + # ------------------------------------------------------------------ # + candidate_headings = [ + line.strip() for line in all_text.splitlines() + if 3 <= len(line.strip()) <= 80 + and line.strip()[0].isupper() + and not line.strip().endswith(".") + and not line.strip().endswith(",") + and len(line.strip().split()) <= 10 + ] + heading_density = len(candidate_headings) / total_pages if total_pages > 0 else 0 + + if heading_density >= 1.0: + results.append(("Struttura logica riconoscibile (desiderabile)", "PASS", + f"~{len(candidate_headings)} possibili titoli rilevati ({heading_density:.1f}/pagina)")) + elif heading_density >= 0.3: + results.append(("Struttura logica riconoscibile (desiderabile)", "WARN", + f"~{len(candidate_headings)} possibili titoli ({heading_density:.1f}/pagina) — struttura parziale")) + else: + results.append(("Struttura logica riconoscibile (desiderabile)", "WARN", + "Pochi titoli rilevati — testo narrativo o struttura non standard")) + + _render_results(results, out) + _maybe_save(lines, path, save) + + +def _render_results(results: list, out) -> None: + icons = {"PASS": "✅", "WARN": "⚠️ ", "FAIL": "❌"} + out() + for label, status, message in results: + icon = icons.get(status, " ") + out(f" {icon} {label}") + out(f" {message}") + out() + + fails = [r for r in results if r[1] == "FAIL"] + warns = [r for r in results if r[1] == "WARN"] + + if fails: + out("ESITO: ❌ PDF NON IDONEO") + out(" Criteri obbligatori non soddisfatti — scegli un PDF diverso.") + elif warns: + out("ESITO: ⚠️ PDF ACCETTABILE CON CAUTELA") + out(" Procedi, ma aspettati più lavoro nella revisione manuale (step 4).") + else: + out("ESITO: ✅ PDF IDONEO") + out(" Tutti i criteri soddisfatti — procedi con lo step 1.") + out() + + +def _maybe_save(lines: list, pdf_path: Path, save: bool) -> None: + if not save: + return + script_dir = Path(__file__).parent + out_file = script_dir / f"{pdf_path.stem}_step0_report.txt" + out_file.write_text("\n".join(lines), encoding="utf-8") + print(f"Report salvato in: {out_file}") + + +if __name__ == "__main__": + project_root = Path(__file__).parent.parent + sources_dir = project_root / "sources" + + if not sources_dir.exists(): + print(f"Errore: cartella sources/ non trovata in {project_root}") + sys.exit(1) + + pdfs = sorted(sources_dir.glob("*.pdf")) + if not pdfs: + print(f"Errore: nessun PDF trovato in {sources_dir}") + sys.exit(1) + + for pdf in pdfs: + check_pdf(str(pdf), save=True) + if len(pdfs) > 1: + print("-" * 50)