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>
89 lines
2.9 KiB
Python
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)
|