PitGlobal/PDF_Layout_Inspector
0
1import flet as ft2import os3import sys4import glob5import json6import threading7import zipfile8 9sys.path.insert(0, os.path.join(os.path.dirname(__file__), "backend"))10 11 12from ocr_loader import run_ocr13from line_detector import run_line_detection14from text_line_checker import run_text_line_check15 16# ─────────────────────────────────────────────17# DIRECTORIES18# Uploaded PDFs land in UPLOAD_DIR (managed by Flet).19# Processed images are saved in OUTPUT_DIR, which lives inside20# ASSETS_DIR so Flet serves them as static files at /output/<name>.png21# ─────────────────────────────────────────────22UPLOAD_DIR = "uploads"23ASSETS_DIR = "assets"24OUTPUT_DIR = os.path.join(ASSETS_DIR, "output")25 26for _d in [UPLOAD_DIR, ASSETS_DIR, OUTPUT_DIR]:27 os.makedirs(_d, exist_ok=True)28 29 30# ─────────────────────────────────────────────31# PIPELINE — processes one PDF through all 4 steps32# ─────────────────────────────────────────────33def process_pdf(pdf_path: str, pdf_name: str, log_fn, done_fn):34 """35 Runs the full inspection pipeline on a single PDF.36 log_fn(msg) → called for each log line37 done_fn(name, paths, ok) → called when done (paths = list of output image paths)38 """39 try:40 # ── STEP 1: OCR ──────────────────────────────41 log_fn(f"[{pdf_name}] Step 1 — AWS Textract OCR...")42 run_ocr(pdf_path)43 44 with open("output.json", encoding="utf-8") as f:45 aws_data = json.load(f)46 47 num_pages = len(aws_data)48 log_fn(f"[{pdf_name}] {num_pages} page(s) found")49 50 output_paths = []51 52 for i in range(num_pages):53 page_num = i + 154 image_file = f"pagina_{page_num}.png"55 56 # ── STEP 2: Line detection ────────────────57 log_fn(f"[{pdf_name}] Page {page_num} — Step 2: Line detection...")58 run_line_detection(image_file)59 60 # ── STEP 3: Text-line check ───────────────61 log_fn(f"[{pdf_name}] Page {page_num} — Step 3: Text-line check...")62 error_image = run_text_line_check(image_file, page_index=i)63 64 out_name = f"{pdf_name}_{page_num}.png" if num_pages > 1 else f"{pdf_name}.png"65 out_path = os.path.join(OUTPUT_DIR, out_name)66 os.replace(error_image, out_path) # oppure shutil.copyfile(error_image, out_path)67 output_paths.append(out_path)68 69 70 # ── CLEANUP intermediate files ────────────────71 for f in (72 glob.glob("pagina_*.png")73 + glob.glob("linee_*.json")74 +[75 "output.json",76 "debug_righe.png",77 ]78 ):79 if os.path.exists(f):80 os.remove(f)81 82 log_fn(f"[{pdf_name}] Done — {len(output_paths)} result image(s) ready.")83 done_fn(pdf_name, output_paths, success=True)84 85 except Exception as ex:86 log_fn(f"[{pdf_name}] ERROR: {ex}")87 done_fn(pdf_name, [], success=False)88 89def create_download_helper(out_name: str, img_url: str) -> str:90 """Crea un file HTML in assets che auto-triggera il download dell'immagine."""91 html = (92 f'<!DOCTYPE html><html><body>'93 f'<script>var a=document.createElement("a");a.href="{img_url}";'94 f'a.download="{out_name}";document.body.appendChild(a);a.click();</script>'95 f'<p>Downloading {out_name}... you can close this tab.</p>'96 f'</body></html>'97 )98 with open(os.path.join(ASSETS_DIR, f"dl_{out_name}.html"), "w") as f:99 f.write(html)100 return f"/dl_{out_name}.html"101 102# ─────────────────────────────────────────────103# FLET APP104# ─────────────────────────────────────────────105def main(page: ft.Page):106 107 page.title = "PDF Layout Inspector"108 page.horizontal_alignment = ft.CrossAxisAlignment.CENTER109 page.bgcolor = "#F8FAFC"110 page.padding = ft.padding.symmetric(horizontal=40, vertical=40)111 page.scroll = ft.ScrollMode.AUTO112 113 # ── State ────────────────────────────────────────114 selected_files: list[str] = [] 115 pending_uploads: set[str] = set()116 117 upload_map: dict[str, str] = {}118 119 # ─────────────────────────────────────────────────120 # UI COMPONENTS121 # ─────────────────────────────────────────────────122 123 # ── Log box ──────────────────────────────────────124 log_col = ft.Column(spacing=2, scroll=ft.ScrollMode.AUTO)125 log_box = ft.Container(126 content=log_col,127 bgcolor="#0F172A",128 border_radius=10,129 padding=ft.padding.all(14),130 height=210,131 width=700,132 visible=False,133 )134 135 def add_log(msg: str):136 log_col.controls.append(137 ft.Text(138 f"› {msg}",139 size=11,140 color="#86EFAC",141 selectable=True,142 font_family="Courier New",143 )144 )145 log_box.visible = True146 page.update()147 148 # ── File list ─────────────────────────────────────149 file_list_col = ft.Column(spacing=6, width=700)150 upload_counter = ft.Text("", size=13, color="#4F46E5", weight=ft.FontWeight.W_500)151 152 def refresh_files():153 file_list_col.controls.clear()154 for fp in selected_files:155 name = os.path.basename(fp)156 file_list_col.controls.append(157 ft.Container(158 content=ft.Row(159 [160 ft.Icon(161 ft.icons.PICTURE_AS_PDF_ROUNDED,162 color="#EF4444",163 size=20,164 ),165 ft.Text(166 name,167 size=13,168 color="#1E293B",169 expand=True,170 ),171 ft.Icon(172 ft.icons.CHECK_CIRCLE_OUTLINE_ROUNDED,173 color="#22C55E",174 size=16,175 ),176 ],177 spacing=10,178 ),179 bgcolor="#F1F5F9",180 border_radius=8,181 padding=ft.padding.symmetric(horizontal=14, vertical=10),182 )183 )184 start_btn.disabled = len(selected_files) == 0185 n = len(selected_files)186 upload_counter.value = f"{n} document{'s' if n != 1 else ''} uploaded" if n > 0 else ""187 page.update()188 189 # ── Results section ───────────────────────────────190 results_col = ft.Column(191 spacing=20,192 horizontal_alignment=ft.CrossAxisAlignment.CENTER,193 )194 195 def add_result_card(pdf_name: str, out_paths: list, success: bool):196 if not success or not out_paths:197 results_col.controls.append(198 ft.Container(199 content=ft.Row(200 [201 ft.Icon(ft.icons.ERROR_OUTLINE_ROUNDED, color="#EF4444"),202 ft.Text(203 f"{pdf_name} — processing failed.",204 color="#EF4444",205 size=13,206 ),207 ],208 spacing=8,209 ),210 width=700,211 padding=ft.padding.symmetric(horizontal=10, vertical=8),212 bgcolor="#FEF2F2",213 border_radius=8,214 border=ft.border.all(1, "#FECACA"),215 )216 )217 page.update()218 return219 220 for out_path in out_paths:221 out_name = os.path.basename(out_path)222 # Flet serves assets_dir files → /output/<name>.png223 img_url = f"/output/{out_name}"224 225 card = ft.Card(226 content=ft.Container(227 content=ft.Column(228 [229 # Card header230 ft.Row(231 [232 ft.Icon(233 ft.icons.FIND_IN_PAGE_ROUNDED,234 color="#4F46E5",235 size=20,236 ),237 ft.Text(238 out_name,239 size=14,240 weight=ft.FontWeight.W_600,241 color="#1E293B",242 expand=True,243 ),244 ft.Container(245 content=ft.Text(246 "Inspected",247 size=11,248 color="#16A34A",249 weight=ft.FontWeight.W_500,250 ),251 bgcolor="#DCFCE7",252 border_radius=20,253 padding=ft.padding.symmetric(254 horizontal=10, vertical=4255 ),256 ),257 ],258 spacing=10,259 ),260 ft.Divider(height=1, color="#E2E8F0"),261 # Preview image262 ft.Container(263 content=ft.Image(264 src=img_url,265 width=660,266 fit=ft.ImageFit.CONTAIN,267 border_radius=6,268 ),269 alignment=ft.alignment.center,270 ),271 ft.Divider(height=1, color="#E2E8F0"),272 # Download button273 ft.Row(274 [275 ft.ElevatedButton(276 "Download Result",277 icon=ft.icons.DOWNLOAD_ROUNDED,278 on_click=lambda _, u=img_url, n=out_name: page.launch_url(279 create_download_helper(n, u)280 ),281 bgcolor="#4F46E5",282 color="#FFFFFF",283 height=40,284 style=ft.ButtonStyle(285 shape=ft.RoundedRectangleBorder(radius=8)286 ),287 )288 ],289 alignment=ft.MainAxisAlignment.END,290 ),291 ],292 spacing=14,293 ),294 padding=20,295 width=700,296 ),297 elevation=2,298 surface_tint_color="#FFFFFF",299 )300 results_col.controls.append(card)301 302 page.update()303 304 # ── Progress / status ────────────────────────────305 status_label = ft.Text("", size=13, color="#64748B")306 307 # ── FilePicker ───────────────────────────────────308 file_picker = ft.FilePicker(309 on_result=lambda e: on_files_picked(e),310 on_upload=lambda e: on_upload_progress(e),311 )312 page.overlay.append(file_picker)313 314 def reset_session():315 selected_files.clear()316 pending_uploads.clear()317 upload_map.clear()318 319 file_list_col.controls.clear()320 results_col.controls.clear()321 log_col.controls.clear()322 log_box.visible = False323 status_label.value = ""324 start_btn.disabled = True325 326 for f in glob.glob(os.path.join(UPLOAD_DIR, "*")):327 if os.path.isfile(f):328 os.remove(f)329 330 for f in glob.glob(os.path.join(OUTPUT_DIR, "*")):331 if os.path.isfile(f):332 os.remove(f)333 334 zip_path = os.path.join(ASSETS_DIR, "all_results.zip")335 if os.path.exists(zip_path):336 os.remove(zip_path)337 338 page.update()339 340 341 def on_files_picked(e: ft.FilePickerResultEvent):342 if not e.files:343 return344 345 reset_session()346 347 upload_list = []348 for f in e.files:349 unique_name = f"{threading.get_ident()}_{f.name}"350 upload_map[f.name] = unique_name351 pending_uploads.add(unique_name)352 url = page.get_upload_url(unique_name, 600)353 upload_list.append(354 ft.FilePickerUploadFile(name=f.name, upload_url=url)355 )356 357 file_picker.upload(upload_list)358 359 def on_upload_progress(e: ft.FilePickerUploadEvent):360 if e.progress == 1.0:361 unique_name = upload_map.get(e.file_name, e.file_name)362 pending_uploads.discard(unique_name)363 fp = os.path.join(UPLOAD_DIR, unique_name)364 if fp not in selected_files:365 selected_files.append(fp)366 refresh_files()367 368 # ── Start button ─────────────────────────────────369 def on_start(_):370 if not selected_files:371 return372 373 start_btn.disabled = True374 clear_btn.disabled = True375 results_col.controls.clear()376 log_col.controls.clear()377 log_box.visible = False378 status_label.value = "Processing..."379 page.update()380 381 def run_all():382 total = len(selected_files)383 for idx, pdf_path in enumerate(selected_files, 1):384 pdf_name = os.path.splitext(os.path.basename(pdf_path))[0]385 status_label.value = f"Processing {idx} of {total}: {pdf_name}"386 page.update()387 process_pdf(388 pdf_path,389 pdf_name,390 log_fn=add_log,391 done_fn=add_result_card,392 )393 394 all_pngs = glob.glob(os.path.join(OUTPUT_DIR, "*.png"))395 if all_pngs:396 zip_path = os.path.join(ASSETS_DIR, "all_results.zip")397 with zipfile.ZipFile(zip_path, "w") as zf:398 for p in all_pngs:399 zf.write(p, os.path.basename(p))400 results_col.controls.append(401 ft.ElevatedButton(402 "Download All Results",403 icon=ft.icons.DOWNLOAD_FOR_OFFLINE_ROUNDED,404 on_click=lambda _: page.launch_url("/all_results.zip"),405 bgcolor="#059669",406 color="#FFFFFF",407 height=46,408 style=ft.ButtonStyle(shape=ft.RoundedRectangleBorder(radius=10)),409 )410 )411 page.update()412 413 414 start_btn.disabled = False415 clear_btn.disabled = False416 status_label.value = (417 f"✓ Inspection complete — {total} document(s) processed."418 )419 page.update()420 421 threading.Thread(target=run_all, daemon=True).start()422 423 start_btn = ft.ElevatedButton(424 "Start Inspection",425 icon=ft.icons.PLAY_ARROW_ROUNDED,426 on_click=on_start,427 disabled=True,428 bgcolor="#4F46E5",429 color="#FFFFFF",430 height=46,431 style=ft.ButtonStyle(shape=ft.RoundedRectangleBorder(radius=10)),432 )433 434 # ── Clear button ──────────────────────────────────435 def on_clear(_):436 selected_files.clear()437 pending_uploads.clear()438 file_list_col.controls.clear()439 results_col.controls.clear()440 log_col.controls.clear()441 log_box.visible = False442 status_label.value = ""443 start_btn.disabled = True444 page.update()445 446 clear_btn = ft.TextButton(447 "Clear all",448 on_click=on_clear,449 style=ft.ButtonStyle(color="#94A3B8"),450 )451 452 # ── Upload zone ───────────────────────────────────453 upload_zone = ft.Container(454 content=ft.Column(455 [456 ft.Icon(ft.icons.CLOUD_UPLOAD_ROUNDED, size=52, color="#4F46E5"),457 ft.Text(458 "Click to upload PDF files",459 size=15,460 weight=ft.FontWeight.W_600,461 color="#1E293B",462 ),463 ft.Text(464 "Multiple documents supported — PDF only",465 size=12,466 color="#94A3B8",467 ),468 ],469 horizontal_alignment=ft.CrossAxisAlignment.CENTER,470 spacing=8,471 ),472 width=700,473 border=ft.border.all(2, "#C7D2FE"),474 border_radius=14,475 bgcolor="#EEF2FF",476 padding=ft.padding.symmetric(vertical=40),477 alignment=ft.alignment.center,478 on_click=lambda _: file_picker.pick_files(479 allowed_extensions=["pdf"],480 allow_multiple=True,481 ),482 ink=True,483 )484 485 # ─────────────────────────────────────────────────486 # PAGE LAYOUT487 # ─────────────────────────────────────────────────488 page.add(489 ft.Column(490 [491 # ── Header ──492 ft.Container(height=8),493 ft.Text(494 "PDF Layout Inspector",495 size=32,496 weight=ft.FontWeight.BOLD,497 color="#1E293B",498 text_align=ft.TextAlign.CENTER,499 ),500 ft.Text(501 "Detect text overlapping structural lines in PDF documents",502 size=14,503 color="#64748B",504 text_align=ft.TextAlign.CENTER,505 ),506 ft.Container(height=6),507 ft.Divider(color="#E2E8F0", height=1),508 ft.Container(height=14),509 510 # ── Upload ──511 upload_zone,512 ft.Container(height=12),513 file_list_col,514 upload_counter,515 ft.Row(516 [clear_btn],517 width=700,518 alignment=ft.MainAxisAlignment.END,519 ),520 ft.Container(height=10),521 522 # ── Action row ──523 ft.Row(524 [start_btn, status_label],525 spacing=18,526 alignment=ft.MainAxisAlignment.CENTER,527 width=700,528 ),529 ft.Container(height=10),530 531 # ── Log ──532 log_box,533 ft.Container(height=24),534 535 # ── Results ──536 results_col,537 ft.Container(height=50),538 ],539 horizontal_alignment=ft.CrossAxisAlignment.CENTER,540 spacing=0,541 )542 )543 544 545# ─────────────────────────────────────────────546# ENTRY POINT547# ─────────────────────────────────────────────548ft.app(549 target=main,550 view=ft.AppView.WEB_BROWSER,551 port=7860,552 upload_dir=UPLOAD_DIR,553 assets_dir=ASSETS_DIR,554)