Files
Conflict_Checker/backend/pipeline/pdf_processor.py
T
woogiandClaude Opus 4.8 1d248a8808 Initial commit: Conflict Checker
Cross-discipline design-contradiction checker for construction drawing
sets. Standalone tool broken out from Iron_Bid; a pipeline stage may
later fold back into Iron_Bid.

Pipeline: PDF->images -> per-sheet assertion extraction -> deterministic
clustering by location -> per-cluster reasoning -> report.
Includes CLI (cli/run_check.py) and web UI (backend/main.py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:22:02 +00:00

89 lines
2.9 KiB
Python

"""
pdf_processor.py - Converts an uploaded PDF drawing set into page images.
Each page becomes a base64-encoded JPEG that the vision model can read.
Higher DPI = better accuracy but slower/heavier requests. Adapted from the
IronBid pipeline (AI_Takeoffs); behavior is intentionally identical so the
two projects stay comparable.
"""
import base64
from io import BytesIO
from typing import List, Dict
from pdf2image import convert_from_path
from PIL import Image
from backend import config
def convert_pdf_to_images(pdf_path: str) -> List[Dict]:
"""
Convert each page of a PDF into a base64-encoded JPEG.
Returns a list of dicts:
{ "page_number": int, "base64": str, "width": int, "height": int }
"""
print(f"[PDF] Converting {pdf_path} at {config.PDF_DPI} DPI...")
pages = convert_from_path(
pdf_path,
dpi=config.PDF_DPI,
fmt="jpeg",
thread_count=4,
use_pdftocairo=True,
)
results: List[Dict] = []
total = min(len(pages), config.MAX_PAGES)
for i, page in enumerate(pages[:total]):
# 2400px gives ~100px/inch on a 24x36 sheet, the minimum needed to
# read 8pt room annotations and dimension strings.
page = _resize_if_needed(page, max_dimension=config.MAX_DIMENSION)
buffer = BytesIO()
page.save(buffer, format="JPEG", quality=85)
b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
results.append({
"page_number": i + 1,
"base64": b64,
"width": page.width,
"height": page.height,
})
print(f"[PDF] Page {i+1}/{total} encoded ({page.width}x{page.height})")
print(f"[PDF] Done - {len(results)} pages ready for analysis")
return results
def render_page_jpeg(pdf_path: str, page: int, dpi: int = 150,
max_dimension: int = 3000) -> bytes:
"""
Render a single 1-based page of a PDF to JPEG bytes, for the sheet viewer.
Higher DPI than the analysis pass (default 150) so text is legible on
screen. Raises IndexError if the page is out of range.
"""
if page < 1:
raise IndexError(f"page {page} out of range")
pages = convert_from_path(pdf_path, dpi=dpi, fmt="jpeg",
first_page=page, last_page=page, use_pdftocairo=True)
if not pages:
raise IndexError(f"page {page} out of range")
img = _resize_if_needed(pages[0], max_dimension=max_dimension)
buffer = BytesIO()
img.save(buffer, format="JPEG", quality=85)
return buffer.getvalue()
def _resize_if_needed(image: Image.Image, max_dimension: int = 2400) -> Image.Image:
"""Resize image proportionally if either dimension exceeds max_dimension."""
w, h = image.size
if w <= max_dimension and h <= max_dimension:
return image
ratio = min(max_dimension / w, max_dimension / h)
new_size = (int(w * ratio), int(h * ratio))
return image.resize(new_size, Image.LANCZOS)